@ashdev/codex-plugin-release-nyaa 1.20.0 → 1.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -558,6 +558,25 @@ function createReleaseSourcePlugin(options) {
558
558
  var RELEASES_METHODS = {
559
559
  /** List tracked series, scoped to what the plugin's manifest declared. */
560
560
  LIST_TRACKED: "releases/list_tracked",
561
+ /**
562
+ * Count tracked series scoped to the plugin's `requiresExternalIds`.
563
+ *
564
+ * Plugins call this once at the start of a poll to learn the total
565
+ * denominator before iterating, so subsequent `REPORT_PROGRESS` calls
566
+ * carry a stable `current/total` ratio. Cheap (one batched DB lookup);
567
+ * safe to call from `poll`.
568
+ */
569
+ COUNT_TRACKED: "releases/count_tracked",
570
+ /**
571
+ * Report intra-poll progress to the host. The host translates this into
572
+ * a `TaskProgressEvent` on the active task's broadcaster; the inbox
573
+ * progress bar updates live. Best-effort — calls outside an active
574
+ * task scope are silently dropped, and rapid back-to-back calls are
575
+ * rate-limited (~10/sec) by the host. Plugins SHOULD call this after
576
+ * each unit of work (e.g. after each polled series) with `current` set
577
+ * to the count of completed units and `total` from `COUNT_TRACKED`.
578
+ */
579
+ REPORT_PROGRESS: "releases/report_progress",
561
580
  /** Submit a candidate to the host's release ledger. */
562
581
  RECORD: "releases/record",
563
582
  /** Get persisted per-source state (etag, last_polled_at, last_error). */
@@ -699,7 +718,7 @@ async function fetchSubscriptionFeed(subscription, previousEtag, previousLastMod
699
718
  // package.json
700
719
  var package_default = {
701
720
  name: "@ashdev/codex-plugin-release-nyaa",
702
- version: "1.20.0",
721
+ version: "1.21.1",
703
722
  description: "Nyaa.si uploader-feed release-source plugin for Codex - announces torrent releases for tracked series, filtered by an admin allowlist of trusted uploaders",
704
723
  main: "dist/index.js",
705
724
  bin: "dist/index.js",
@@ -739,7 +758,7 @@ var package_default = {
739
758
  node: ">=22.0.0"
740
759
  },
741
760
  dependencies: {
742
- "@ashdev/codex-plugin-sdk": "^1.20.0"
761
+ "@ashdev/codex-plugin-sdk": "^1.21.1"
743
762
  },
744
763
  devDependencies: {
745
764
  "@biomejs/biome": "^2.4.4",
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../node_modules/@ashdev/codex-plugin-sdk/src/types/rpc.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/errors.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/request-context.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/host-rpc.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/logger.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/server.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/storage.ts", "../node_modules/@ashdev/codex-plugin-sdk/src/types/releases.ts", "../src/fetcher.ts", "../package.json", "../src/manifest.ts", "../src/matcher.ts", "../src/parser.ts", "../src/index.ts"],
4
- "sourcesContent": [null, null, null, null, null, null, null, null, "/**\n * Nyaa.si RSS fetcher.\n *\n * Wraps `fetch` with conditional GET (`If-None-Match` from a stored ETag, plus\n * `If-Modified-Since` from a stored Last-Modified header) and a hard timeout.\n *\n * Nyaa exposes two feed shapes we care about:\n * - User feed: `https://nyaa.si/?page=rss&u=<username>`\n * - Search feed: `https://nyaa.si/?page=rss&q=<query>` (with optional\n * filters; the plugin keeps it simple and lets aliases\n * do the matching)\n *\n * Returns a discriminated result so the caller can:\n * - act on `200`: parse the body, persist the new ETag.\n * - skip parse on `304`: nothing changed since last poll.\n * - report `429` / `5xx` upstream-status codes back to the host so the\n * per-host backoff layer can react.\n *\n * Network is the only side effect; nothing in here touches storage, the host,\n * or process state. That keeps it trivially testable: pass a mocked `fetch`\n * implementation and assert.\n */\n\n/** Discriminated fetch result. */\nexport type FetchResult =\n | { kind: \"ok\"; body: string; etag: string | null; lastModified: string | null; status: 200 }\n | { kind: \"notModified\"; status: 304 }\n | { kind: \"error\"; status: number; message: string };\n\nexport interface FetcherOptions {\n /** Custom `fetch` impl (for testing). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n /** Per-request timeout. Defaults to 10s. */\n timeoutMs?: number;\n /** Override base URL (for tests / mirrors). Defaults to `https://nyaa.si`. */\n baseUrl?: string;\n}\n\n/** Default Nyaa base URL. */\nexport const NYAA_BASE_URL = \"https://nyaa.si\";\n\n/**\n * One uploader subscription entry.\n *\n * Three shapes:\n * - `user` \u2014 pulls `?page=rss&u=<identifier>` (a Nyaa user feed).\n * - `query` \u2014 pulls `?page=rss&q=<identifier>` (a plain text search).\n * - `params` \u2014 pulls `?page=rss&<params>` where `<params>` is an\n * allowlisted set of Nyaa query keys (`q`, `c`, `f`). Used to express\n * category / filter combinations like the Literature \u2192 English-translated\n * view (`c=3_1`).\n */\nexport type UploaderSubscription =\n | { kind: \"user\"; identifier: string }\n | { kind: \"query\"; identifier: string }\n | { kind: \"params\"; identifier: string };\n\n/**\n * Keys allowed through from a `q:?\u2026` URL-style token. `page` is always\n * injected by the plugin and can't be overridden; anything not in this set\n * is silently dropped to keep the surface tight.\n */\nconst PARAMS_ALLOWLIST = new Set([\"q\", \"c\", \"f\", \"u\"]);\n\n/**\n * Parse a `q:?key=value&\u2026` body into a normalized, allowlisted query string.\n * Returns null when no allowlisted keys remain (caller drops the token).\n *\n * Normalization sorts params alphabetically so two tokens that differ only\n * in key order dedupe to the same identifier.\n */\nfunction parseUrlParams(body: string): { kind: \"user\" | \"params\"; identifier: string } | null {\n const params = new URLSearchParams(body);\n const kept: [string, string][] = [];\n for (const [rawKey, rawValue] of params.entries()) {\n const key = rawKey.toLowerCase();\n if (!PARAMS_ALLOWLIST.has(key)) continue;\n const value = rawValue.trim();\n if (value.length === 0) continue;\n kept.push([key, value]);\n }\n if (kept.length === 0) return null;\n\n // If the *only* allowlisted key is `u`, collapse to a plain user token so\n // `q:?u=1r0n` dedupes against the bare `1r0n` form and reuses the same\n // URL-building branch.\n if (kept.length === 1 && kept[0]?.[0] === \"u\") {\n return { kind: \"user\", identifier: kept[0][1] };\n }\n\n kept.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n const normalized = new URLSearchParams(kept).toString();\n return { kind: \"params\", identifier: normalized };\n}\n\n/**\n * Parse a single uploader subscription token.\n *\n * Tokens look like:\n * - `1r0n` \u2192 user feed\n * - `q:LuminousScans` \u2192 plain search query\n * - `query:Manga Group` \u2192 plain search query (long form)\n * - `q:?c=3_1&q=Berserk` \u2192 URL-style params (allowlisted: q, c, f, u)\n * - `query:?u=1r0n&c=3_1` \u2192 URL-style params, treated as user feed\n *\n * The leading `?` after `q:` / `query:` is the opt-in switch into URL mode,\n * which keeps `q:c=3_1&q=Berserk` (no `?`) parsing as a literal search term\n * for backwards compatibility.\n *\n * Empty / whitespace-only tokens return null (caller should drop them).\n */\nexport function parseSubscriptionToken(raw: string): UploaderSubscription | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n\n // `q:` / `query:` prefix \u2192 search query, in either plain or URL-params form.\n const prefixMatch = trimmed.match(/^(q|query):(.*)$/i);\n if (prefixMatch) {\n const body = (prefixMatch[2] ?? \"\").trim();\n if (body.length === 0) return null;\n\n if (body.startsWith(\"?\")) {\n return parseUrlParams(body.slice(1));\n }\n return { kind: \"query\", identifier: body };\n }\n\n // Plain identifier \u2192 username feed.\n return { kind: \"user\", identifier: trimmed };\n}\n\n/**\n * Build a stable per-plugin source key for a subscription. Mirrors the\n * dedup key used in `parseSubscriptionList` so two ways of writing the\n * same subscription collapse to the same source row.\n *\n * Used by `releases/register_sources` (to declare the plugin-owned key for\n * each row) and as a fallback when reconstructing a subscription from a\n * source key whose `config` is missing. Lower-cased identifier preserves\n * the existing case-insensitive dedup behaviour.\n */\nexport function subscriptionToSourceKey(sub: UploaderSubscription): string {\n return `${sub.kind}:${sub.identifier.toLowerCase()}`;\n}\n\n/**\n * Inverse of `subscriptionToSourceKey`: parse a `kind:identifier` source key\n * back into a subscription. Returns null for unrecognized keys (older rows\n * from a previous plugin version, manual edits, etc.) so the caller can log\n * and skip without crashing the whole poll.\n *\n * Note: the identifier coming back is lower-cased (per the source key\n * convention). Nyaa is case-insensitive on usernames and search terms, so\n * the round-trip is lossless for our purposes.\n */\nexport function sourceKeyToSubscription(key: string): UploaderSubscription | null {\n const idx = key.indexOf(\":\");\n if (idx <= 0 || idx === key.length - 1) return null;\n const kind = key.slice(0, idx);\n const identifier = key.slice(idx + 1);\n if (kind === \"user\" || kind === \"query\" || kind === \"params\") {\n return { kind, identifier };\n }\n return null;\n}\n\n/**\n * Parse the admin `uploaders` config into a clean list of subscriptions.\n *\n * Accepts either a JSON array (preferred \u2014 what the manifest now declares) or\n * a legacy comma-separated string. The string path is retained so existing\n * stored configs and CLI/env-driven setups keep working without a migration.\n *\n * Skips empty tokens; preserves order; deduplicates case-insensitively.\n */\nexport function parseSubscriptionList(raw: unknown): UploaderSubscription[] {\n let tokens: string[];\n if (Array.isArray(raw)) {\n tokens = raw.filter((t): t is string => typeof t === \"string\");\n } else if (typeof raw === \"string\") {\n tokens = raw.split(\",\");\n } else {\n return [];\n }\n\n const seen = new Set<string>();\n const out: UploaderSubscription[] = [];\n for (const token of tokens) {\n const sub = parseSubscriptionToken(token);\n if (sub === null) continue;\n const key = subscriptionToSourceKey(sub);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(sub);\n }\n return out;\n}\n\n/** Build the per-subscription RSS URL. */\nexport function feedUrl(\n subscription: UploaderSubscription,\n baseUrl: string = NYAA_BASE_URL,\n): string {\n const base = baseUrl.replace(/\\/+$/, \"\");\n if (subscription.kind === \"user\") {\n return `${base}/?page=rss&u=${encodeURIComponent(subscription.identifier)}`;\n }\n if (subscription.kind === \"query\") {\n return `${base}/?page=rss&q=${encodeURIComponent(subscription.identifier)}`;\n }\n // params: identifier is already a URL-encoded, allowlisted query string.\n return `${base}/?page=rss&${subscription.identifier}`;\n}\n\n/**\n * Conditional GET against an uploader-subscription RSS feed.\n *\n * @param subscription - The uploader subscription to fetch.\n * @param previousEtag - The ETag from the previous successful poll (if any).\n * @param previousLastModified - Optional Last-Modified header from the previous\n * poll. Nyaa often returns one but doesn't always honor `If-None-Match`;\n * sending both maximizes 304 hit rate.\n * @param opts - Fetcher options (custom fetch, timeout, base URL override).\n */\nexport async function fetchSubscriptionFeed(\n subscription: UploaderSubscription,\n previousEtag: string | null,\n previousLastModified: string | null,\n opts: FetcherOptions = {},\n): Promise<FetchResult> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n const timeoutMs = opts.timeoutMs ?? 10_000;\n const baseUrl = opts.baseUrl ?? NYAA_BASE_URL;\n\n const url = feedUrl(subscription, baseUrl);\n const headers: Record<string, string> = {\n Accept: \"application/rss+xml, application/xml;q=0.9, */*;q=0.5\",\n \"User-Agent\": \"Codex-ReleaseTracker/1.0 (+https://github.com/AshDevFr/codex)\",\n };\n if (previousEtag) {\n headers[\"If-None-Match\"] = previousEtag;\n }\n if (previousLastModified) {\n headers[\"If-Modified-Since\"] = previousLastModified;\n }\n\n const signal = AbortSignal.timeout(timeoutMs);\n\n let resp: Response;\n try {\n resp = await fetchImpl(url, { method: \"GET\", headers, signal });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown fetch error\";\n return { kind: \"error\", status: 0, message: msg };\n }\n\n if (resp.status === 304) {\n return { kind: \"notModified\", status: 304 };\n }\n\n if (resp.status === 200) {\n const body = await resp.text();\n const etag = resp.headers.get(\"etag\");\n const lastModified = resp.headers.get(\"last-modified\");\n return { kind: \"ok\", body, etag, lastModified, status: 200 };\n }\n\n return {\n kind: \"error\",\n status: resp.status,\n message: `upstream returned ${resp.status} ${resp.statusText}`,\n };\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-release-nyaa\",\n \"version\": \"1.20.0\",\n \"description\": \"Nyaa.si uploader-feed release-source plugin for Codex - announces torrent releases for tracked series, filtered by an admin allowlist of trusted uploaders\",\n \"main\": \"dist/index.js\",\n \"bin\": \"dist/index.js\",\n \"type\": \"module\",\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/AshDevFr/codex.git\",\n \"directory\": \"plugins/release-nyaa\"\n },\n \"scripts\": {\n \"build\": \"esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'\",\n \"dev\": \"npm run build -- --watch\",\n \"clean\": \"rm -rf dist\",\n \"start\": \"node dist/index.js\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --passWithNoTests\",\n \"test:watch\": \"vitest\",\n \"prepublishOnly\": \"npm run lint && npm run build\"\n },\n \"keywords\": [\n \"codex\",\n \"plugin\",\n \"nyaa\",\n \"release-source\",\n \"manga\",\n \"torrent\"\n ],\n \"author\": \"Codex\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"dependencies\": {\n \"@ashdev/codex-plugin-sdk\": \"^1.20.0\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.4.4\",\n \"@types/node\": \"^22.0.0\",\n \"esbuild\": \"^0.27.3\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n }\n}\n", "import type { PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\n/** Default per-fetch HTTP timeout. Nyaa is usually fast; 10s is generous. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Default minimum confidence threshold for emitted candidates. Nyaa matches\n * series via title parsing + alias comparison, which is fuzzier than the\n * external-ID match used by MangaUpdates. The host's threshold (default 0.7)\n * still filters at record time; this is the plugin-side floor below which we\n * don't even bother calling `releases/record`.\n */\nexport const DEFAULT_MIN_CONFIDENCE = 0.7;\n\nexport const manifest = {\n name: \"release-nyaa\",\n displayName: \"Nyaa Releases\",\n version: packageJson.version,\n description:\n \"Announces new chapter / volume torrents for tracked series via Nyaa.si uploader RSS feeds. Limited to an admin-configured uploader allowlist; matches via title aliases.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n releaseSource: {\n kinds: [\"rss-uploader\"],\n requiresAliases: true,\n canAnnounceChapters: true,\n canAnnounceVolumes: true,\n },\n },\n configSchema: {\n description:\n \"Nyaa plugin configuration. The plugin polls the listed uploaders' RSS feeds (or, for groups without a Nyaa account, a fallback search query) and emits release candidates only for tracked series whose aliases match the parsed title. Notification-only: Codex never downloads torrents.\",\n fields: [\n {\n key: \"uploaders\",\n label: \"Uploader Subscriptions\",\n description:\n \"List of trusted uploader handles or queries. Each entry is one of: `username` (a Nyaa user feed); `q:<query>` (a plain site-wide search); or `q:?<params>` (URL-style allowlisted params: `q`, `c`, `f`, `u` \u2014 e.g. `q:?c=3_1&q=Berserk` to search the Literature \u2192 English-translated category). Accepts a JSON array (preferred) or a legacy comma-separated string. Confidence stays above the rejection threshold only for entries that match a tracked series alias.\",\n type: \"string-array\" as const,\n required: false,\n default: [],\n example: [\"1r0n\", \"TankobonBlur\", \"q:LuminousScans\", \"q:?c=3_1&q=Berserk\"],\n },\n {\n key: \"requestTimeoutMs\",\n label: \"Request Timeout (ms)\",\n description:\n \"How long to wait for a single Nyaa RSS fetch before giving up. Defaults to 10000 (10 seconds).\",\n type: \"number\" as const,\n required: false,\n default: DEFAULT_REQUEST_TIMEOUT_MS,\n },\n {\n key: \"baseUrl\",\n label: \"Nyaa Base URL\",\n description:\n \"Override the Nyaa base URL. Useful for mirrors or for tests. Defaults to https://nyaa.si.\",\n type: \"string\" as const,\n required: false,\n default: \"https://nyaa.si\",\n example: \"https://nyaa.si\",\n },\n ],\n },\n userDescription:\n \"Watches Nyaa.si uploader feeds for new releases of tracked series. Matches by title alias \u2014 make sure your series' aliases (auto-populated from metadata or added manually in the Tracking panel) cover the way the uploader names them. Notification-only \u2014 Codex never downloads anything.\",\n adminSetupInstructions:\n \"1. Set the **Uploaders** config field to a JSON array of entries (a comma-separated string is still accepted for backwards compatibility). Each entry is one of: `username` (a Nyaa user feed, e.g. `tsuna69`), `q:<query>` (a plain site-wide search, e.g. `q:LuminousScans`), or `q:?<params>` (URL-style search with allowlisted keys `q`, `c`, `f`, `u`, e.g. `q:?c=3_1&q=Berserk` for the English-translated Literature category). 2. Save. The plugin restarts and the host materializes one row per entry in **Settings \u2192 Release tracking** \u2014 that's where you flip rows on/off, override the poll interval, or hit *Poll now*. 3. Make sure tracked series have aliases that match how the uploader names releases (alternate spellings, romanizations, volume-range tags). The plugin auto-prunes rows when you remove an entry from the list and re-save, so the Release tracking table stays in sync with this list.\",\n} as const satisfies PluginManifest & {\n capabilities: { releaseSource: { kinds: [\"rss-uploader\"] } };\n};\n", "/**\n * Alias matcher for Nyaa releases.\n *\n * Nyaa identifies series only by name in the torrent title \u2014 there's no\n * `nyaa_id` or other stable external ID that ties a release to a specific\n * series in our DB. So matching is a two-step pipeline:\n *\n * 1. Normalize the parsed `seriesGuess` and every alias the host returned\n * to a common shape (lowercase, alphanumeric + spaces only). This\n * mirrors the `normalize_alias` function on the host\n * ([src/db/entities/series_aliases.rs](src/db/entities/series_aliases.rs))\n * so a release whose normalized title exactly matches one of a series'\n * stored aliases lands at confidence 0.95.\n * 2. If no exact match, compute a token-level S\u00F8rensen-Dice similarity\n * against every candidate alias. The highest ratio wins, scaled into a\n * 0.7..0.85 confidence band; below the configured threshold we skip.\n *\n * The Dice ratio is more forgiving than edit distance for word-rearranged\n * titles (`\"Boruto Two Blue Vortex\"` vs. `\"Boruto - Two Blue Vortex\"`) while\n * still rejecting unrelated series at the threshold. We deliberately don't\n * wire a heavy fuzzy-match library; the surface area is small.\n */\n\n/** A tracked-series candidate with its raw aliases. */\nexport interface AliasCandidate {\n /** Codex series UUID. */\n seriesId: string;\n /** Raw aliases from `releases/list_tracked`. */\n aliases: string[];\n}\n\n/** A successful match. */\nexport interface AliasMatch {\n seriesId: string;\n confidence: number;\n /** Reason string surfaced in the SeriesMatch \u2014 \"alias-exact\" or \"alias-fuzzy\". */\n reason: string;\n /** The matched alias (raw form, for logging). */\n matchedAlias: string;\n}\n\n/**\n * Confidence assigned on an exact normalized match.\n *\n * Below 1.0 because we still don't have an external ID \u2014 a release titled\n * `\"X\"` could legitimately match multiple series with that alias. The host's\n * threshold treats this as a strong-but-not-certain signal.\n */\nexport const CONFIDENCE_EXACT = 0.95;\n\n/**\n * Floor below which fuzzy matches don't get emitted. The host's default\n * threshold is 0.7; we share that floor so plugin-side filtering doesn't\n * silently second-guess host config.\n */\nexport const DEFAULT_FUZZY_FLOOR = 0.7;\n\n/**\n * Anything below this Dice-coefficient is rejected outright (even before the\n * confidence floor kicks in). 0.85 lets through \"two-blue-vortex\" vs. \"two\n * blue vortex\" but kills \"naruto\" vs. \"boruto two blue vortex\".\n */\nexport const MIN_DICE_RATIO = 0.85;\n\n// ---------------------------------------------------------------------------\n// Normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize an alias to the same shape the host stores in\n * `series_aliases.normalized`. Mirrors the Rust `normalize_alias` impl \u2014 keep\n * these in lockstep.\n */\nexport function normalizeAlias(input: string): string {\n let out = \"\";\n let lastWasSpace = false;\n for (const ch of input) {\n // Match Rust's `is_alphanumeric()` (Unicode-aware).\n if (/[\\p{L}\\p{N}]/u.test(ch)) {\n out += ch.toLowerCase();\n lastWasSpace = false;\n } else if (/\\s/.test(ch) && out.length > 0 && !lastWasSpace) {\n out += \" \";\n lastWasSpace = true;\n }\n // Anything else (punctuation, control, symbols) is dropped.\n }\n return out.endsWith(\" \") ? out.slice(0, -1) : out;\n}\n\n// ---------------------------------------------------------------------------\n// Dice coefficient (token-level, character-bigram fallback)\n// ---------------------------------------------------------------------------\n\n/**\n * S\u00F8rensen-Dice coefficient on word-bigrams of the input strings (with a\n * character-bigram fallback for short / single-word strings).\n *\n * Range: 0..1, where 1.0 means identical bigram sets.\n */\nexport function diceRatio(a: string, b: string): number {\n if (a.length === 0 || b.length === 0) return 0;\n if (a === b) return 1;\n\n const bigramsA = bigrams(a);\n const bigramsB = bigrams(b);\n if (bigramsA.size === 0 || bigramsB.size === 0) return 0;\n\n let intersection = 0;\n for (const bg of bigramsA) {\n if (bigramsB.has(bg)) intersection++;\n }\n return (2 * intersection) / (bigramsA.size + bigramsB.size);\n}\n\nfunction bigrams(s: string): Set<string> {\n const out = new Set<string>();\n // Word bigrams first.\n const words = s.split(/\\s+/).filter((w) => w.length > 0);\n if (words.length >= 2) {\n for (let i = 0; i < words.length - 1; i++) {\n out.add(`${words[i]} ${words[i + 1]}`);\n }\n }\n // Plus character bigrams to handle word-rearrangement and short strings.\n const flat = s.replace(/\\s+/g, \"\");\n if (flat.length >= 2) {\n for (let i = 0; i < flat.length - 1; i++) {\n out.add(`#${flat.slice(i, i + 2)}`);\n }\n } else if (flat.length === 1) {\n out.add(`#${flat}`);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Public matching entry point\n// ---------------------------------------------------------------------------\n\nexport interface MatchOptions {\n /**\n * Minimum confidence for a fuzzy match to be returned. Defaults to\n * `DEFAULT_FUZZY_FLOOR` (0.7). Below this, the matcher returns null.\n */\n fuzzyFloor?: number;\n}\n\n/**\n * Match a parsed series-guess against a list of tracked-series candidates and\n * their aliases. Returns the best match or null if nothing clears the floor.\n *\n * On an exact normalized match against any alias of a candidate, confidence\n * is `CONFIDENCE_EXACT` (0.95). If multiple candidates have aliases that\n * normalize to the same form, the first one wins \u2014 that's a data-quality\n * issue the host surfaces via the `latest_known_*` advance gate, not\n * something the matcher can untangle alone.\n *\n * On no exact match, the matcher computes Dice ratios across the cartesian\n * product (candidates \u00D7 aliases), finds the maximum, scales it from\n * `[MIN_DICE_RATIO, 1.0]` into `[fuzzyFloor, 0.85]`, and returns a fuzzy\n * match if the result is at or above the floor.\n */\nexport function matchSeries(\n seriesGuess: string,\n candidates: AliasCandidate[],\n opts: MatchOptions = {},\n): AliasMatch | null {\n const floor = opts.fuzzyFloor ?? DEFAULT_FUZZY_FLOOR;\n const target = normalizeAlias(seriesGuess);\n if (target.length === 0 || candidates.length === 0) return null;\n\n // Pass 1 \u2014 exact normalized match.\n for (const c of candidates) {\n for (const alias of c.aliases) {\n if (normalizeAlias(alias) === target) {\n return {\n seriesId: c.seriesId,\n confidence: CONFIDENCE_EXACT,\n reason: \"alias-exact\",\n matchedAlias: alias,\n };\n }\n }\n }\n\n // Pass 2 \u2014 best fuzzy match.\n let best: AliasMatch | null = null;\n let bestRatio = 0;\n for (const c of candidates) {\n for (const alias of c.aliases) {\n const ratio = diceRatio(target, normalizeAlias(alias));\n if (ratio > bestRatio) {\n bestRatio = ratio;\n best = {\n seriesId: c.seriesId,\n confidence: 0,\n reason: \"alias-fuzzy\",\n matchedAlias: alias,\n };\n }\n }\n }\n if (best === null || bestRatio < MIN_DICE_RATIO) return null;\n\n // Linearly scale [MIN_DICE_RATIO..1.0] \u2192 [fuzzyFloor..0.85].\n // (We cap the fuzzy ceiling below CONFIDENCE_EXACT so an alias-exact match\n // is always strictly stronger than the best alias-fuzzy match.)\n const ceiling = 0.85;\n const span = 1 - MIN_DICE_RATIO;\n const t = (bestRatio - MIN_DICE_RATIO) / span; // 0..1 inside the band\n const confidence = floor + t * (ceiling - floor);\n if (confidence < floor) return null;\n best.confidence = Number(confidence.toFixed(4));\n return best;\n}\n\n/**\n * Match a list of alias guesses (e.g. from a `Title A / Title B` Nyaa title)\n * and return the best result across them.\n *\n * Picks the highest-confidence match across all guesses, preferring\n * `alias-exact` over `alias-fuzzy` when ties exist (because exact carries a\n * fixed `CONFIDENCE_EXACT` and fuzzy is bounded below it). When two guesses\n * both produce alias-exact matches against different series, the first guess\n * wins \u2014 that's the same precedence rule `matchSeries` applies internally\n * across candidates.\n */\nexport function matchSeriesAny(\n seriesGuesses: string[],\n candidates: AliasCandidate[],\n opts: MatchOptions = {},\n): AliasMatch | null {\n if (seriesGuesses.length === 0) return null;\n let best: AliasMatch | null = null;\n for (const guess of seriesGuesses) {\n const m = matchSeries(guess, candidates, opts);\n if (m === null) continue;\n if (best === null || m.confidence > best.confidence) {\n best = m;\n }\n }\n return best;\n}\n", "/**\n * RSS parser for Nyaa.si feeds.\n *\n * Nyaa's RSS namespace exposes one extra element per item that we care about\n * (`<nyaa:infoHash>`), plus the standard `<title>`, `<link>`, `<guid>`,\n * `<pubDate>`, and `<description>` fields. We pull all of them with the same\n * lightweight regex pipeline used for MangaUpdates \u2014 no heavy XML dep.\n *\n * Parsing the title is where most of the work is. Real-world examples\n * (sourced from production Nyaa feeds and the user's screenshot of 1r0n's\n * subscription):\n *\n * \"[1r0n] Boruto - Two Blue Vortex - Volume 02 (Digital) (1r0n)\"\n * \"[1r0n] One Piece v107 (Digital)\"\n * \"[1r0n] Chainsaw Man - Chapter 142 (Digital)\"\n * \"[Group] Dandadan c126-142 (2024) (Digital)\"\n * \"[Tankobon Blur] Solo Leveling Vol. 13 (2024) (Digital) (Tankobon Blur)\"\n * \"Berserk Volume 42 (Digital)\"\n *\n * The shape we want out of each item:\n * - parsed series guess (alias-free string used for matching)\n * - chapter / volume axes (decimals supported on chapter)\n * - format hints (Digital / JXL / etc.)\n * - uploader-tagged group (if encoded as a leading `[Group]` token)\n *\n * Nyaa titles are noisy; we keep parsing best-effort and surface confidence\n * downstream from the alias matcher rather than failing here.\n */\n\n/**\n * Numeric inclusive range. Single values are encoded as `start === end`\n * (`{ start: 5, end: 5 }`).\n */\nexport interface NumericSpan {\n start: number;\n end: number;\n}\n\n/** Parsed item, pre-`ReleaseCandidate`. */\nexport interface ParsedRssItem {\n /** Stable per-source ID. Derived from the link or guid. */\n externalReleaseId: string;\n /** Original title. Useful for debugging / fallback. */\n title: string;\n /** Series-name guess after stripping volume/chapter/group/format tokens. */\n seriesGuess: string;\n /**\n * All alias candidates extracted from the series-name region. When the title\n * uses `Title A / Title B` (a common 1r0n / LuCaZ convention for \"JP name /\n * EN name\"), both halves are surfaced here so the matcher can score against\n * either. For titles without a slash separator this is a single-element\n * array equal to `[seriesGuess]`.\n */\n seriesGuessAliases: string[];\n /**\n * Volume coverage as a normalized span list. Sorted ascending by `start`,\n * with overlapping spans merged. Disjoint spans (`v01-04 + v06-09`) are\n * preserved as two entries. `null` when the title carried no volume\n * information at all.\n */\n volumes: NumericSpan[] | null;\n /**\n * Chapter coverage as a normalized span list. Same conventions as\n * [`volumes`]; decimals are preserved (`c12.5` \u2192 `[{12.5, 12.5}]`).\n */\n chapters: NumericSpan[] | null;\n /** Leading `[Group]` token, if any. */\n group: string | null;\n /** Format hints as a small dictionary (digital, jxl, ...). */\n formatHints: Record<string, boolean>;\n /** RSS `<link>` value. On Nyaa this is the `.torrent` download URL. */\n link: string;\n /**\n * Permalink to the release post page (e.g. `https://nyaa.si/view/12345`),\n * derived from the `<guid isPermaLink=\"true\">` tag. Null when the guid is\n * missing or doesn't look like a post URL.\n */\n pageUrl: string | null;\n /** `nyaa:infoHash` value, lowercased; null if missing. */\n infoHash: string | null;\n /** ISO-8601 timestamp. Falls back to \"now\" if pubDate is missing/invalid. */\n observedAt: string;\n}\n\n// -----------------------------------------------------------------------------\n// XML helpers (mirror release-mangaupdates conventions)\n// -----------------------------------------------------------------------------\n\nfunction decodeXmlText(raw: string): string {\n let s = raw.trim();\n const cdataMatch = s.match(/^<!\\[CDATA\\[([\\s\\S]*?)]]>$/);\n if (cdataMatch?.[1] !== undefined) {\n s = cdataMatch[1];\n }\n return s\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\");\n}\n\n/** Pull the first `<tag>` text content from an XML fragment, or null. */\nfunction extractTagText(xml: string, tag: string): string | null {\n // Escape `:` for namespaced tags (e.g. `nyaa:infoHash`).\n const safeTag = tag.replace(/:/g, \"\\\\:\");\n const re = new RegExp(`<${safeTag}[^>]*>([\\\\s\\\\S]*?)</${safeTag}>`, \"i\");\n const m = xml.match(re);\n if (!m?.[1]) return null;\n return decodeXmlText(m[1]);\n}\n\nfunction splitItems(xml: string): string[] {\n const out: string[] = [];\n const re = /<item\\b[^>]*>([\\s\\S]*?)<\\/item>/gi;\n for (;;) {\n const match = re.exec(xml);\n if (match === null) break;\n if (match[1] !== undefined) out.push(match[1]);\n }\n return out;\n}\n\n// -----------------------------------------------------------------------------\n// Title parsing\n// -----------------------------------------------------------------------------\n\n/**\n * Strip a leading `[Group]` token off the title and return both pieces.\n * If the title has no leading bracketed token, returns `{ rest: title,\n * group: null }`.\n */\nfunction extractLeadingGroup(title: string): { rest: string; group: string | null } {\n const m = title.match(/^\\s*\\[([^\\]]+)\\]\\s*(.*)$/);\n if (!m?.[1]) return { rest: title, group: null };\n const group = m[1].trim();\n const rest = m[2] ?? \"\";\n return { rest, group: group.length > 0 ? group : null };\n}\n\n/**\n * Strip every `(...)` group from a string. Used to keep year ranges, uploader\n * credits, and format-hint tags out of the chapter/volume tokenizer \u2014 those\n * always live inside parentheses, so anything inside them must not be\n * interpreted as release-info.\n */\nfunction stripParens(s: string): string {\n return s.replace(/\\([^)]*\\)/g, \" \");\n}\n\n/**\n * Locate the start of the \"release-info span\" \u2014 the offset in `s` (which has\n * already had `(...)` groups blanked) where chapter/volume tokens begin.\n *\n * Anchors, in priority order:\n * 1. A `v##`, `vol.##`, `volume ##` token (with or without a range).\n * 2. A bare numeric range with both sides at 3+ digits (`031-037`,\n * `001-069`). Two-digit forms are rejected to avoid false positives\n * inside series names (`30s`, `My 100`, etc.).\n * 3. A `c##` / `ch.##` / `Chapter ##` token.\n *\n * Returns the index of the anchor, or -1 if no release-info is present (the\n * whole string is then treated as a series name).\n */\nfunction findReleaseInfoStart(s: string): number {\n const anchors: RegExp[] = [\n /\\b(?:v|vol|volume)\\.?\\s*[0-9]+/i,\n /\\b[0-9]{3,4}\\s*[-\u2013]\\s*[0-9]{3,4}\\b/,\n /\\b(?:c|ch|chapter)\\.?\\s*[0-9]+/i,\n ];\n let best = -1;\n for (const re of anchors) {\n const m = s.match(re);\n if (m && m.index !== undefined && (best === -1 || m.index < best)) {\n best = m.index;\n }\n }\n return best;\n}\n\n/**\n * Spread tokens are the comma- / `+`- / whitespace- / `as`-separated atoms\n * that make up the release-info span:\n *\n * - `volume` : single volume number (`v01`, `Vol. 13`)\n * - `volRange` : volume range (`v01-14`)\n * - `chapter` : single chapter number (`c143`, bare `70`)\n * - `chapRange` : chapter range (`c126-142`, bare `031-037`)\n *\n * The tokenizer scans left-to-right and consumes one token per match. Bare\n * numeric tokens are only accepted *after* the release-info anchor \u2014 see\n * `findReleaseInfoStart` \u2014 so series-name digits don't leak in.\n */\ntype SpreadToken =\n | { kind: \"volume\"; value: number }\n | { kind: \"volRange\"; start: number; end: number }\n | { kind: \"chapter\"; value: number }\n | { kind: \"chapRange\"; start: number; end: number };\n\n/**\n * Tokenize the release-info span into volume/chapter atoms.\n *\n * `s` should be the parens-stripped substring starting at the release-info\n * anchor. The tokenizer is intentionally permissive about separators (commas,\n * `+`, whitespace, `as`) \u2014 we just consume tokens greedily and aggregate\n * downstream.\n */\nfunction tokenizeReleaseInfo(s: string): SpreadToken[] {\n const tokens: SpreadToken[] = [];\n\n // Match either a prefixed volume/chapter token, or a bare numeric range /\n // single. The order in the alternation matters: ranges must be tried before\n // single tokens, and prefixed forms must be tried before bare numerics so\n // we don't mis-classify `v05` as bare-chapter `5`.\n //\n // 1. `v##-##` / `vol.##-##` / `volume ##-##` \u2192 volRange\n // 2. `v##` / `vol.##` / `volume ##` \u2192 volume\n // 3. `c##.##-##.##` / `ch.##-##` / `Chapter ##-##` \u2192 chapRange\n // 4. `c##.##` / `ch.##` / `Chapter ##` \u2192 chapter\n // 5. bare `###-###` (3+ digits each side) \u2192 chapRange\n // 6. bare `##` (1+ digits) \u2014 only matches *after* the first anchor token\n // has been emitted, see `acceptShortBare` below. Lets us pick up\n // \"extra\" chapters expressed as short numerics (`+ 70`) without\n // promoting incidental name-region digits.\n const tokenRe = new RegExp(\n [\n \"\\\\b(?<vrs>v|vol|volume)\\\\.?\\\\s*([0-9]+)\\\\s*[-\u2013]\\\\s*([0-9]+)\\\\b\",\n \"\\\\b(?<vss>v|vol|volume)\\\\.?\\\\s*([0-9]+)\\\\b\",\n \"\\\\b(?<crs>c|ch|chapter)\\\\.?\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\s*[-\u2013]\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\b\",\n \"\\\\b(?<css>c|ch|chapter)\\\\.?\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\b\",\n \"\\\\b(?<brs>)([0-9]{3,4})\\\\s*[-\u2013]\\\\s*([0-9]{3,4})\\\\b\",\n \"\\\\b(?<bss>)([0-9]{1,4})\\\\b\",\n ].join(\"|\"),\n \"gi\",\n );\n\n for (;;) {\n const m = tokenRe.exec(s);\n if (m === null) break;\n const groups = m.groups ?? {};\n if (groups.vrs !== undefined) {\n const start = Number.parseInt(m[2] ?? \"\", 10);\n const end = Number.parseInt(m[3] ?? \"\", 10);\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"volRange\", start, end });\n }\n continue;\n }\n if (groups.vss !== undefined) {\n const value = Number.parseInt(m[5] ?? \"\", 10);\n if (Number.isFinite(value)) tokens.push({ kind: \"volume\", value });\n continue;\n }\n if (groups.crs !== undefined) {\n const start = Number.parseFloat(m[7] ?? \"\");\n const end = Number.parseFloat(m[8] ?? \"\");\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"chapRange\", start, end });\n }\n continue;\n }\n if (groups.css !== undefined) {\n const value = Number.parseFloat(m[10] ?? \"\");\n if (Number.isFinite(value)) tokens.push({ kind: \"chapter\", value });\n continue;\n }\n if (groups.brs !== undefined) {\n const start = Number.parseInt(m[12] ?? \"\", 10);\n const end = Number.parseInt(m[13] ?? \"\", 10);\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"chapRange\", start, end });\n }\n continue;\n }\n if (groups.bss !== undefined) {\n const raw = m[15] ?? \"\";\n const value = Number.parseInt(raw, 10);\n if (!Number.isFinite(value)) continue;\n // Only accept short (\u22642 digit) bare numerics once we've already\n // committed to a richer token; on its own a `42` is more likely a\n // year fragment or noise than a chapter. 3+ digits is unambiguous in\n // this corpus so we always accept it.\n if (raw.length < 3 && tokens.length === 0) continue;\n tokens.push({ kind: \"chapter\", value });\n }\n }\n\n return tokens;\n}\n\n/**\n * Project the spread-token list onto two axes of normalized [`NumericSpan`]s.\n *\n * Single-value tokens (`v05`, `c143`) become `{ start: N, end: N }`. Range\n * tokens (`v01-09`, `001-050`, `c126-142`) become `{ start, end }` with the\n * lower number on `start`. We do *not* min/max aggregate across an axis any\n * more \u2014 disjoint ranges (`v01-04 + v06-09`) must survive intact so the host\n * can ask \"does the user own everything in here?\" honestly.\n */\nfunction tokensToSpans(tokens: SpreadToken[]): {\n volumes: NumericSpan[] | null;\n chapters: NumericSpan[] | null;\n} {\n const vol: NumericSpan[] = [];\n const chap: NumericSpan[] = [];\n for (const t of tokens) {\n switch (t.kind) {\n case \"volume\":\n vol.push({ start: t.value, end: t.value });\n break;\n case \"volRange\":\n vol.push({ start: t.start, end: t.end });\n break;\n case \"chapter\":\n chap.push({ start: t.value, end: t.value });\n break;\n case \"chapRange\":\n chap.push({ start: t.start, end: t.end });\n break;\n }\n }\n return {\n volumes: vol.length === 0 ? null : normalizeSpans(vol),\n chapters: chap.length === 0 ? null : normalizeSpans(chap),\n };\n}\n\n/**\n * Sort a span list ascending and merge overlapping entries. We deliberately\n * do *not* merge purely adjacent spans (`[1, 4]` and `[5, 9]` stay separate);\n * the uploader chose to write them disjointly and we preserve that intent.\n * Bad inputs where `start > end` get swapped before sorting so downstream\n * iteration always sees `start <= end`.\n */\nfunction normalizeSpans(spans: NumericSpan[]): NumericSpan[] {\n if (spans.length === 0) return spans;\n const fixed = spans.map((s) =>\n s.start <= s.end ? { start: s.start, end: s.end } : { start: s.end, end: s.start },\n );\n fixed.sort((a, b) => a.start - b.start || a.end - b.end);\n const out: NumericSpan[] = [];\n for (const s of fixed) {\n const last = out[out.length - 1];\n if (last !== undefined && s.start <= last.end) {\n // Overlap \u2014 extend the existing span. Equality of endpoints\n // (`[1, 4]` + `[4, 9]`) counts as overlap too.\n if (s.end > last.end) last.end = s.end;\n } else {\n out.push(s);\n }\n }\n return out;\n}\n\n/**\n * Walk the parenthesized tags in the title and extract format hints.\n *\n * Common Nyaa hints we care about:\n * - `(Digital)` \u2192 `digital`\n * - `(JXL)` \u2192 `jxl`\n * - `(Mag-Z)` / `(Magazine)` \u2192 `magazine`\n * - `(Omnibus Edition)` / `(Omnibus)` \u2192 `omnibus`\n * - `(2024)` is a year, ignored (we'd need it for naming dedup but not for filtering)\n */\nfunction extractFormatHints(s: string): Record<string, boolean> {\n const hints: Record<string, boolean> = {};\n const tagRe = /\\(([^)]+)\\)/g;\n for (;;) {\n const match = tagRe.exec(s);\n if (match === null) break;\n const tag = (match[1] ?? \"\").trim().toLowerCase();\n if (tag.length === 0) continue;\n if (tag === \"digital\") hints.digital = true;\n else if (tag === \"jxl\") hints.jxl = true;\n else if (tag === \"magazine\" || tag === \"mag-z\") hints.magazine = true;\n else if (tag === \"webtoon\") hints.webtoon = true;\n else if (tag === \"bw\" || tag === \"b&w\") hints.bw = true;\n else if (tag === \"color\") hints.color = true;\n else if (tag === \"omnibus\" || tag === \"omnibus edition\") hints.omnibus = true;\n }\n return hints;\n}\n\n/**\n * Strip a trailing `[...]` token (e.g. `[Oak]` at the end of some\n * danke-Empire releases). Mirrors `extractLeadingGroup` but at the tail and\n * without surfacing the value \u2014 trailing brackets are credit, not a parsing\n * signal we currently use.\n */\nfunction stripTrailingBracket(s: string): string {\n return s.replace(/\\s*\\[[^\\]]+\\]\\s*$/g, \"\").trim();\n}\n\n/**\n * Take the \"name region\" of a release title (everything before the first\n * release-info anchor, with parens already stripped) and reduce it to a clean\n * primary guess plus alias candidates.\n *\n * The name region may still contain:\n * - subtitle dashes: `Boruto - Two Blue Vortex` \u2192 joined with spaces\n * - alias separator: `Ao no Hako / Blue Box` \u2192 both halves returned\n *\n * Apostrophes and hyphenated words (`Amagami-san`, `Chillin'`) are preserved\n * \u2014 the host's `normalize_alias` strips them at match time, but we want to\n * keep them readable in logs and admin surfaces.\n */\nfunction extractSeriesAliases(nameRegion: string): {\n primary: string;\n aliases: string[];\n} {\n // Subtitle dashes: ` - `, ` \u2013 `, ` \u2014 ` are titling glue, not separators.\n // Joining the halves with a single space mirrors the prior behavior the\n // existing tests assert (`Boruto Two Blue Vortex`).\n const dashJoined = nameRegion.replace(/\\s+[-\u2013\u2014]\\s+/g, \" \");\n\n // Alias separator. Only ` / ` (with whitespace on both sides) splits \u2014 bare\n // `/` survives so e.g. `AC/DC Tales` stays one alias.\n const parts = dashJoined\n .split(/\\s+\\/\\s+/)\n .map((p) => p.replace(/\\s+/g, \" \").trim())\n .filter((p) => p.length > 0);\n\n if (parts.length === 0) return { primary: \"\", aliases: [] };\n return { primary: parts[0] ?? \"\", aliases: parts };\n}\n\n/**\n * Public entry point \u2014 extract the structured fields from a single Nyaa\n * release title.\n *\n * Returns null only if the title is empty after trimming. Otherwise returns a\n * best-effort parse where the series guess may still be empty (e.g. for\n * meta-bundles without a leading series name); the matcher then drops those.\n */\nexport function parseTitle(title: string): {\n seriesGuess: string;\n seriesGuessAliases: string[];\n volumes: NumericSpan[] | null;\n chapters: NumericSpan[] | null;\n group: string | null;\n formatHints: Record<string, boolean>;\n} | null {\n const trimmed = title.trim();\n if (trimmed.length === 0) return null;\n\n const { rest, group } = extractLeadingGroup(trimmed);\n const formatHints = extractFormatHints(rest);\n\n // Blank out `(...)` groups so years and uploader credits can't be picked up\n // by the release-info tokenizer, then split into name region / release-info\n // region at the first chapter/volume anchor.\n const flattened = stripTrailingBracket(stripParens(rest));\n const anchor = findReleaseInfoStart(flattened);\n const nameRegion = anchor === -1 ? flattened : flattened.slice(0, anchor);\n const infoRegion = anchor === -1 ? \"\" : flattened.slice(anchor);\n\n const tokens = tokenizeReleaseInfo(infoRegion);\n const { volumes, chapters } = tokensToSpans(tokens);\n const { primary, aliases } = extractSeriesAliases(nameRegion);\n\n return {\n seriesGuess: primary,\n seriesGuessAliases: aliases.length > 0 ? aliases : [primary],\n volumes,\n chapters,\n group,\n formatHints,\n };\n}\n\n// -----------------------------------------------------------------------------\n// Item parsing\n// -----------------------------------------------------------------------------\n\nfunction pubDateToIso(raw: string | null): string {\n if (raw) {\n const d = new Date(raw);\n if (!Number.isNaN(d.getTime())) return d.toISOString();\n }\n return new Date().toISOString();\n}\n\n/**\n * Pull the post-page URL out of the guid when it looks like a Nyaa\n * `/view/<id>` permalink. The `<link>` tag in Nyaa feeds is the `.torrent`\n * download URL, which is not what we want to surface to users.\n */\nfunction derivePageUrl(guid: string | null): string | null {\n if (!guid) return null;\n const trimmed = guid.trim();\n if (trimmed.length === 0) return null;\n // Match http(s)://<host>/view/<id> with optional trailing slash / query.\n if (/^https?:\\/\\/[^/]+\\/view\\/[^/?#]+/i.test(trimmed)) return trimmed;\n return null;\n}\n\nfunction deriveExternalReleaseId(\n guid: string | null,\n link: string | null,\n infoHash: string | null,\n title: string,\n pubDate: string | null,\n): string {\n if (guid && guid.trim().length > 0) return guid.trim();\n if (link && link.trim().length > 0) return link.trim();\n if (infoHash && infoHash.length > 0) return `urn:btih:${infoHash}`;\n // Deterministic fallback: djb2-ish hash. Same algorithm MangaUpdates uses.\n const fallback = `${title}|${pubDate ?? \"\"}`;\n let h = 5381;\n for (let i = 0; i < fallback.length; i++) {\n h = ((h << 5) + h + fallback.charCodeAt(i)) | 0;\n }\n return `t:${(h >>> 0).toString(36)}`;\n}\n\n/**\n * Parse a single Nyaa `<item>` block. Returns null when the title is missing\n * (truly malformed entry).\n */\nexport function parseItem(itemXml: string): ParsedRssItem | null {\n const title = extractTagText(itemXml, \"title\");\n if (!title) return null;\n\n const link = extractTagText(itemXml, \"link\");\n const guid = extractTagText(itemXml, \"guid\");\n const pubDate = extractTagText(itemXml, \"pubDate\");\n const infoHashRaw = extractTagText(itemXml, \"nyaa:infoHash\");\n const infoHash = infoHashRaw ? infoHashRaw.toLowerCase().trim() : null;\n\n const parsedTitle = parseTitle(title);\n if (parsedTitle === null) return null;\n\n return {\n externalReleaseId: deriveExternalReleaseId(guid, link, infoHash, title, pubDate),\n title,\n seriesGuess: parsedTitle.seriesGuess,\n seriesGuessAliases: parsedTitle.seriesGuessAliases,\n volumes: parsedTitle.volumes,\n chapters: parsedTitle.chapters,\n group: parsedTitle.group,\n formatHints: parsedTitle.formatHints,\n link: link ?? \"\",\n pageUrl: derivePageUrl(guid),\n infoHash,\n observedAt: pubDateToIso(pubDate),\n };\n}\n\n/**\n * Parse a full Nyaa RSS feed body into structured items. Bad items (missing\n * title) are dropped silently \u2014 Nyaa feeds occasionally include broken entries\n * and we'd rather keep going than poison the whole poll.\n */\nexport function parseFeed(xml: string): ParsedRssItem[] {\n return splitItems(xml)\n .map(parseItem)\n .filter((i): i is ParsedRssItem => i !== null);\n}\n", "/**\n * Nyaa.si Release-Source Plugin for Codex.\n *\n * Polls Nyaa user / search RSS feeds for an admin-configured uploader\n * allowlist and announces new releases for tracked series. Matching is\n * alias-based: each parsed Nyaa title is normalized and compared to every\n * tracked series' alias list. Confidence is 0.95 on exact normalized match,\n * dropping to a fuzzy floor of 0.7 for near-matches; below that, the\n * candidate is silently dropped (the host's threshold would reject it\n * anyway).\n *\n * Source-row model:\n * - On `onInitialize` (which the host re-runs after every config save),\n * the plugin parses the admin's `uploaders` CSV and calls\n * `releases/register_sources` with one entry per subscription. The host\n * materializes one `release_sources` row per uploader, keyed on\n * `(plugin_id, sourceKey)` where `sourceKey` is `kind:identifier`\n * (e.g. `user:tsuna69`, `query:luminousscans`, `params:c=3_1&q=berserk`).\n * - The host scheduler fires one `releases/poll` task per source row, so\n * each uploader has its own poll cadence, ETag, and last-error status.\n *\n * Flow per `releases/poll`:\n * 1. Recover the subscription from `params.config.subscription` (or fall\n * back to parsing `params.sourceKey`).\n * 2. Pull tracked-series + aliases from the host\n * (`releases/list_tracked`).\n * 3. Conditional GET the RSS feed using `params.etag`.\n * 4. Parse each item; match against tracked aliases; emit a candidate via\n * `releases/record`.\n * 5. Return the new ETag and upstream status for the host's per-host\n * backoff layer.\n */\n\nimport {\n createLogger,\n createReleaseSourcePlugin,\n type HostRpcClient,\n HostRpcError,\n type InitializeParams,\n RELEASES_METHODS,\n type ReleaseCandidate,\n type ReleasePollRequest,\n type ReleasePollResponse,\n type TrackedSeriesEntry,\n} from \"@ashdev/codex-plugin-sdk\";\nimport {\n fetchSubscriptionFeed,\n parseSubscriptionList,\n sourceKeyToSubscription,\n subscriptionToSourceKey,\n type UploaderSubscription,\n} from \"./fetcher.js\";\nimport { DEFAULT_MIN_CONFIDENCE, DEFAULT_REQUEST_TIMEOUT_MS, manifest } from \"./manifest.js\";\nimport { type AliasCandidate, type AliasMatch, matchSeriesAny } from \"./matcher.js\";\nimport { type ParsedRssItem, parseFeed } from \"./parser.js\";\n\nconst logger = createLogger({ name: manifest.name, level: \"info\" });\n\n// =============================================================================\n// Plugin-level state (set during initialize)\n// =============================================================================\n\ninterface PluginState {\n hostRpc: HostRpcClient | null;\n /** Parsed admin uploader subscription list. */\n subscriptions: UploaderSubscription[];\n /** Hard timeout for upstream fetches. */\n requestTimeoutMs: number;\n /** Minimum confidence floor \u2014 passed to the matcher's `fuzzyFloor`. */\n minConfidence: number;\n /** Override base URL (for tests / mirrors). */\n baseUrl: string | null;\n}\n\nconst state: PluginState = {\n hostRpc: null,\n subscriptions: [],\n requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,\n minConfidence: DEFAULT_MIN_CONFIDENCE,\n baseUrl: null,\n};\n\n/** Reset state. Exported for tests; not part of the plugin contract. */\nexport function _resetState(): void {\n state.hostRpc = null;\n state.subscriptions = [];\n state.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n state.minConfidence = DEFAULT_MIN_CONFIDENCE;\n state.baseUrl = null;\n}\n\n// =============================================================================\n// Reverse-RPC wrappers\n// =============================================================================\n\ninterface ListTrackedResponse {\n tracked: TrackedSeriesEntry[];\n nextOffset?: number;\n}\n\ninterface RecordResponse {\n ledgerId: string;\n deduped: boolean;\n}\n\nasync function listTracked(\n rpc: HostRpcClient,\n sourceId: string,\n offset: number,\n limit: number,\n): Promise<ListTrackedResponse> {\n return rpc.call<ListTrackedResponse>(RELEASES_METHODS.LIST_TRACKED, {\n sourceId,\n offset,\n limit,\n });\n}\n\nasync function recordCandidate(\n rpc: HostRpcClient,\n sourceId: string,\n candidate: ReleaseCandidate,\n): Promise<RecordResponse | null> {\n try {\n return await rpc.call<RecordResponse>(RELEASES_METHODS.RECORD, {\n sourceId,\n candidate,\n });\n } catch (err) {\n if (err instanceof HostRpcError) {\n logger.warn(\n `record failed for ${candidate.externalReleaseId}: ${err.message} (code ${err.code})`,\n );\n } else {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n logger.warn(`record failed for ${candidate.externalReleaseId}: ${msg}`);\n }\n return null;\n }\n}\n\n// =============================================================================\n// Iteration helpers\n// =============================================================================\n\n/**\n * Pull every tracked-series page from the host. We can't stream\n * subscription-by-subscription because each Nyaa item has to be matched\n * against the *full* alias set; partial pages would leak misses.\n */\nexport async function fetchAllTracked(\n rpc: HostRpcClient,\n sourceId: string,\n): Promise<AliasCandidate[]> {\n const out: AliasCandidate[] = [];\n const pageSize = 200;\n let offset = 0;\n while (true) {\n const page = await listTracked(rpc, sourceId, offset, pageSize);\n for (const entry of page.tracked) {\n const aliases = entry.aliases ?? [];\n // Drop entries with no aliases \u2014 Nyaa matching is alias-only.\n if (aliases.length === 0) continue;\n out.push({ seriesId: entry.seriesId, aliases });\n }\n if (page.nextOffset === undefined || page.tracked.length === 0) return out;\n offset = page.nextOffset;\n }\n}\n\n// =============================================================================\n// Per-subscription poll\n// =============================================================================\n\n/** Outcome of a single per-subscription fetch+parse cycle. */\nexport interface SubscriptionPollOutcome {\n subscription: UploaderSubscription;\n fetched: boolean;\n notModified: boolean;\n parsed: number;\n matched: number;\n recorded: number;\n /** Of those sent to record, how many the host deduped onto an existing row. */\n deduped: number;\n upstreamStatus: number;\n /** New ETag returned by upstream (only set when fetched=true). */\n etag: string | null;\n error: string;\n}\n\n/**\n * Build a `ReleaseCandidate` from a parsed RSS item + the matcher's verdict.\n *\n * Language is hardcoded to `\"en\"` \u2014 Nyaa releases don't carry a language tag\n * in the title or RSS metadata. English-only is the right default for the\n * uploader allowlist this plugin is designed around (`1r0n`, etc.); admins\n * who add non-English uploaders should configure tracked series' languages\n * accordingly. The host's `latest_known_*` advance gate enforces the\n * per-series language list.\n */\nfunction toCandidate(\n match: AliasMatch,\n item: ParsedRssItem,\n subscription: UploaderSubscription,\n): ReleaseCandidate {\n // Format hints carry just the recognized parser tags (`digital`, `jxl`, \u2026)\n // plus the originating subscription. Volume / chapter coverage now travels\n // as first-class span lists on the candidate itself rather than smuggled\n // through `format_hints.*RangeEnd` keys.\n const formatHints: Record<string, unknown> = { ...item.formatHints };\n formatHints.subscription = `${subscription.kind}:${subscription.identifier}`;\n\n // Nyaa RSS carries two URLs per item:\n // <guid>: the human-readable post page (`/view/<id>`)\n // <link>: the actual `.torrent` download\n // We surface the page as `payloadUrl` (the inbox's external-link icon)\n // and the torrent as `mediaUrl` with kind=torrent so the UI can render a\n // second, kind-specific icon for one-click acquisition. When the page URL\n // is missing we fall back to the torrent for `payloadUrl` and skip the\n // separate media link to avoid pointing both icons at the same URL.\n const torrentLink = item.link.length > 0 ? item.link : null;\n const payloadUrl = item.pageUrl ?? torrentLink ?? `urn:nyaa:${item.externalReleaseId}`;\n const hasDistinctMedia = item.pageUrl !== null && torrentLink !== null;\n\n return {\n seriesMatch: {\n codexSeriesId: match.seriesId,\n confidence: match.confidence,\n reason: match.reason,\n },\n externalReleaseId: item.externalReleaseId,\n volumes: item.volumes,\n chapters: item.chapters,\n language: \"en\",\n groupOrUploader: item.group ?? (subscription.kind === \"user\" ? subscription.identifier : null),\n payloadUrl,\n ...(hasDistinctMedia ? { mediaUrl: torrentLink, mediaUrlKind: \"torrent\" as const } : {}),\n infoHash: item.infoHash,\n formatHints,\n observedAt: item.observedAt,\n };\n}\n\n/**\n * Poll a single uploader subscription. Internal \u2014 exposed for testing.\n */\nexport async function pollSubscription(\n rpc: HostRpcClient,\n sourceId: string,\n subscription: UploaderSubscription,\n candidates: AliasCandidate[],\n options: {\n previousEtag: string | null;\n timeoutMs: number;\n minConfidence: number;\n baseUrl?: string | null;\n fetchImpl?: typeof fetch;\n },\n): Promise<SubscriptionPollOutcome> {\n const result = await fetchSubscriptionFeed(subscription, options.previousEtag, null, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),\n });\n\n if (result.kind === \"notModified\") {\n return {\n subscription,\n fetched: true,\n notModified: true,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: 304,\n etag: null,\n error: \"\",\n };\n }\n\n if (result.kind === \"error\") {\n return {\n subscription,\n fetched: false,\n notModified: false,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: result.status,\n etag: null,\n error: result.message,\n };\n }\n\n // result.kind === \"ok\"\n const items = parseFeed(result.body);\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n for (const item of items) {\n // Prefer the alias-list form: a `Title A / Title B` Nyaa title surfaces\n // both halves in `seriesGuessAliases`, so the matcher can hit on either\n // the JP or EN side of the alias separator. Falls back to the single\n // guess for titles without a slash.\n const guesses =\n item.seriesGuessAliases.length > 0 ? item.seriesGuessAliases : [item.seriesGuess];\n const m = matchSeriesAny(guesses, candidates, {\n fuzzyFloor: options.minConfidence,\n });\n if (m === null) continue;\n matched++;\n const candidate = toCandidate(m, item, subscription);\n const outcome = await recordCandidate(rpc, sourceId, candidate);\n if (!outcome) continue;\n if (outcome.deduped) {\n deduped++;\n } else {\n recorded++;\n }\n }\n return {\n subscription,\n fetched: true,\n notModified: false,\n parsed: items.length,\n matched,\n recorded,\n deduped,\n upstreamStatus: 200,\n etag: result.etag,\n error: \"\",\n };\n}\n\n// =============================================================================\n// Top-level poll handler\n// =============================================================================\n\n/**\n * Resolve the subscription this poll request is for. The host stamps every\n * `release_sources` row with its plugin-defined `config` (set at register\n * time), so the preferred path is `params.config.subscription`. If a row\n * pre-dates the config field (e.g. created in a previous plugin version),\n * fall back to parsing `params.sourceKey`.\n */\nfunction resolveSubscription(params: ReleasePollRequest): UploaderSubscription | null {\n const cfg = params.config as { subscription?: unknown } | undefined | null;\n const fromConfig = cfg?.subscription;\n if (fromConfig && typeof fromConfig === \"object\") {\n const obj = fromConfig as Record<string, unknown>;\n const kind = obj.kind;\n const identifier = obj.identifier;\n if (\n typeof identifier === \"string\" &&\n identifier.length > 0 &&\n (kind === \"user\" || kind === \"query\" || kind === \"params\")\n ) {\n return { kind, identifier };\n }\n }\n if (typeof params.sourceKey === \"string\" && params.sourceKey.length > 0) {\n return sourceKeyToSubscription(params.sourceKey);\n }\n return null;\n}\n\nasync function poll(params: ReleasePollRequest, rpc: HostRpcClient): Promise<ReleasePollResponse> {\n const sourceId = params.sourceId;\n const subscription = resolveSubscription(params);\n if (subscription === null) {\n logger.warn(`source=${sourceId} no resolvable subscription on poll request; skipping`);\n return { notModified: false, upstreamStatus: 200 };\n }\n\n // 1. Pull tracked-series + aliases.\n const tracked = await fetchAllTracked(rpc, sourceId);\n if (tracked.length === 0) {\n logger.info(`no tracked series with aliases for source=${sourceId}`);\n return { notModified: false, upstreamStatus: 200 };\n }\n\n // 2. Conditional GET against this subscription's feed.\n const outcome = await pollSubscription(rpc, sourceId, subscription, tracked, {\n previousEtag: params.etag ?? null,\n timeoutMs: state.requestTimeoutMs,\n minConfidence: state.minConfidence,\n ...(state.baseUrl ? { baseUrl: state.baseUrl } : {}),\n });\n if (outcome.error) {\n logger.warn(\n `source=${sourceId} ${subscription.kind}:${subscription.identifier}: ${outcome.error} (status ${outcome.upstreamStatus})`,\n );\n }\n\n logger.info(\n `poll complete: source=${sourceId} subscription=${subscription.kind}:${subscription.identifier} tracked=${tracked.length} parsed=${outcome.parsed} matched=${outcome.matched} recorded=${outcome.recorded} deduped=${outcome.deduped} status=${outcome.upstreamStatus}${outcome.notModified ? \" (304)\" : \"\"}`,\n );\n\n // Report counters back to the host so it can build a meaningful\n // `last_summary` for the source. Without these, the host only sees the\n // (empty) `candidates` payload \u2014 we record via reverse-RPC mid-poll \u2014\n // and the status badge reads \"Fetched 0 items\" even on a busy poll.\n return {\n notModified: outcome.notModified,\n upstreamStatus: outcome.upstreamStatus,\n parsed: outcome.parsed,\n matched: outcome.matched,\n recorded: outcome.recorded,\n deduped: outcome.deduped,\n ...(outcome.etag !== null ? { etag: outcome.etag } : {}),\n };\n}\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\n/**\n * Send the desired-state list of source rows to the host. Called from\n * `onInitialize` (after the host has installed the releases reverse-RPC\n * handler) so the plugin's source rows are materialized whenever the\n * config changes.\n *\n * Retries on `METHOD_NOT_FOUND` with linear backoff: the host installs the\n * releases handler shortly after `initialize` returns, and there is a small\n * race window where the plugin's first reverse-RPC call may land before the\n * handler is in place.\n */\nexport async function registerSources(\n rpc: HostRpcClient,\n subscriptions: UploaderSubscription[],\n): Promise<{ registered: number; pruned: number } | null> {\n const sources = subscriptions.map((sub) => ({\n sourceKey: subscriptionToSourceKey(sub),\n displayName: displayNameFor(sub),\n kind: \"rss-uploader\" as const,\n config: { subscription: { kind: sub.kind, identifier: sub.identifier } },\n }));\n\n const maxAttempts = 5;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await rpc.call<{ registered: number; pruned: number }>(\n RELEASES_METHODS.REGISTER_SOURCES,\n { sources },\n );\n } catch (err) {\n const isMethodNotFound = err instanceof HostRpcError && err.code === -32601;\n if (isMethodNotFound && attempt < maxAttempts) {\n // Wait for the host to finish installing the releases reverse-RPC\n // handler. Linear backoff: 50ms, 100ms, 150ms, 200ms.\n await new Promise((r) => setTimeout(r, 50 * attempt));\n continue;\n }\n const reason = err instanceof Error ? err.message : String(err);\n logger.error(`register_sources failed: ${reason}`);\n return null;\n }\n }\n return null;\n}\n\n/** Human-readable label shown in the Release tracking settings table. */\nfunction displayNameFor(sub: UploaderSubscription): string {\n if (sub.kind === \"user\") return `Nyaa: ${sub.identifier}`;\n if (sub.kind === \"query\") return `Nyaa search: ${sub.identifier}`;\n return `Nyaa params: ${sub.identifier}`;\n}\n\ncreateReleaseSourcePlugin({\n manifest,\n provider: {\n async poll(params: ReleasePollRequest): Promise<ReleasePollResponse> {\n if (!state.hostRpc) {\n throw new Error(\"Plugin not initialized: hostRpc client missing\");\n }\n return poll(params, state.hostRpc);\n },\n },\n logLevel: \"info\",\n async onInitialize(params: InitializeParams) {\n state.hostRpc = params.hostRpc;\n const ac = params.adminConfig ?? {};\n state.subscriptions = parseSubscriptionList(ac.uploaders);\n if (typeof ac.requestTimeoutMs === \"number\" && Number.isFinite(ac.requestTimeoutMs)) {\n state.requestTimeoutMs = Math.max(1_000, Math.min(ac.requestTimeoutMs, 60_000));\n }\n if (typeof ac.baseUrl === \"string\" && ac.baseUrl.trim().length > 0) {\n state.baseUrl = ac.baseUrl.trim();\n }\n logger.info(\n `initialized: subscriptions=${state.subscriptions.length} timeoutMs=${state.requestTimeoutMs} minConfidence=${state.minConfidence}`,\n );\n\n // Materialize source rows. Deferred to a microtask + retry on\n // METHOD_NOT_FOUND so we run *after* the host installs the releases\n // reverse-RPC handler (it does so right after `initialize` returns).\n queueMicrotask(() => {\n void registerSources(params.hostRpc, state.subscriptions).then((result) => {\n if (result) {\n logger.info(`register_sources: registered=${result.registered} pruned=${result.pruned}`);\n }\n });\n });\n },\n});\n\nlogger.info(\"Nyaa release-source plugin started\");\n"],
5
- "mappings": "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",
4
+ "sourcesContent": [null, null, null, null, null, null, null, null, "/**\n * Nyaa.si RSS fetcher.\n *\n * Wraps `fetch` with conditional GET (`If-None-Match` from a stored ETag, plus\n * `If-Modified-Since` from a stored Last-Modified header) and a hard timeout.\n *\n * Nyaa exposes two feed shapes we care about:\n * - User feed: `https://nyaa.si/?page=rss&u=<username>`\n * - Search feed: `https://nyaa.si/?page=rss&q=<query>` (with optional\n * filters; the plugin keeps it simple and lets aliases\n * do the matching)\n *\n * Returns a discriminated result so the caller can:\n * - act on `200`: parse the body, persist the new ETag.\n * - skip parse on `304`: nothing changed since last poll.\n * - report `429` / `5xx` upstream-status codes back to the host so the\n * per-host backoff layer can react.\n *\n * Network is the only side effect; nothing in here touches storage, the host,\n * or process state. That keeps it trivially testable: pass a mocked `fetch`\n * implementation and assert.\n */\n\n/** Discriminated fetch result. */\nexport type FetchResult =\n | { kind: \"ok\"; body: string; etag: string | null; lastModified: string | null; status: 200 }\n | { kind: \"notModified\"; status: 304 }\n | { kind: \"error\"; status: number; message: string };\n\nexport interface FetcherOptions {\n /** Custom `fetch` impl (for testing). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n /** Per-request timeout. Defaults to 10s. */\n timeoutMs?: number;\n /** Override base URL (for tests / mirrors). Defaults to `https://nyaa.si`. */\n baseUrl?: string;\n}\n\n/** Default Nyaa base URL. */\nexport const NYAA_BASE_URL = \"https://nyaa.si\";\n\n/**\n * One uploader subscription entry.\n *\n * Three shapes:\n * - `user` \u2014 pulls `?page=rss&u=<identifier>` (a Nyaa user feed).\n * - `query` \u2014 pulls `?page=rss&q=<identifier>` (a plain text search).\n * - `params` \u2014 pulls `?page=rss&<params>` where `<params>` is an\n * allowlisted set of Nyaa query keys (`q`, `c`, `f`). Used to express\n * category / filter combinations like the Literature \u2192 English-translated\n * view (`c=3_1`).\n */\nexport type UploaderSubscription =\n | { kind: \"user\"; identifier: string }\n | { kind: \"query\"; identifier: string }\n | { kind: \"params\"; identifier: string };\n\n/**\n * Keys allowed through from a `q:?\u2026` URL-style token. `page` is always\n * injected by the plugin and can't be overridden; anything not in this set\n * is silently dropped to keep the surface tight.\n */\nconst PARAMS_ALLOWLIST = new Set([\"q\", \"c\", \"f\", \"u\"]);\n\n/**\n * Parse a `q:?key=value&\u2026` body into a normalized, allowlisted query string.\n * Returns null when no allowlisted keys remain (caller drops the token).\n *\n * Normalization sorts params alphabetically so two tokens that differ only\n * in key order dedupe to the same identifier.\n */\nfunction parseUrlParams(body: string): { kind: \"user\" | \"params\"; identifier: string } | null {\n const params = new URLSearchParams(body);\n const kept: [string, string][] = [];\n for (const [rawKey, rawValue] of params.entries()) {\n const key = rawKey.toLowerCase();\n if (!PARAMS_ALLOWLIST.has(key)) continue;\n const value = rawValue.trim();\n if (value.length === 0) continue;\n kept.push([key, value]);\n }\n if (kept.length === 0) return null;\n\n // If the *only* allowlisted key is `u`, collapse to a plain user token so\n // `q:?u=1r0n` dedupes against the bare `1r0n` form and reuses the same\n // URL-building branch.\n if (kept.length === 1 && kept[0]?.[0] === \"u\") {\n return { kind: \"user\", identifier: kept[0][1] };\n }\n\n kept.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n const normalized = new URLSearchParams(kept).toString();\n return { kind: \"params\", identifier: normalized };\n}\n\n/**\n * Parse a single uploader subscription token.\n *\n * Tokens look like:\n * - `1r0n` \u2192 user feed\n * - `q:LuminousScans` \u2192 plain search query\n * - `query:Manga Group` \u2192 plain search query (long form)\n * - `q:?c=3_1&q=Berserk` \u2192 URL-style params (allowlisted: q, c, f, u)\n * - `query:?u=1r0n&c=3_1` \u2192 URL-style params, treated as user feed\n *\n * The leading `?` after `q:` / `query:` is the opt-in switch into URL mode,\n * which keeps `q:c=3_1&q=Berserk` (no `?`) parsing as a literal search term\n * for backwards compatibility.\n *\n * Empty / whitespace-only tokens return null (caller should drop them).\n */\nexport function parseSubscriptionToken(raw: string): UploaderSubscription | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n\n // `q:` / `query:` prefix \u2192 search query, in either plain or URL-params form.\n const prefixMatch = trimmed.match(/^(q|query):(.*)$/i);\n if (prefixMatch) {\n const body = (prefixMatch[2] ?? \"\").trim();\n if (body.length === 0) return null;\n\n if (body.startsWith(\"?\")) {\n return parseUrlParams(body.slice(1));\n }\n return { kind: \"query\", identifier: body };\n }\n\n // Plain identifier \u2192 username feed.\n return { kind: \"user\", identifier: trimmed };\n}\n\n/**\n * Build a stable per-plugin source key for a subscription. Mirrors the\n * dedup key used in `parseSubscriptionList` so two ways of writing the\n * same subscription collapse to the same source row.\n *\n * Used by `releases/register_sources` (to declare the plugin-owned key for\n * each row) and as a fallback when reconstructing a subscription from a\n * source key whose `config` is missing. Lower-cased identifier preserves\n * the existing case-insensitive dedup behaviour.\n */\nexport function subscriptionToSourceKey(sub: UploaderSubscription): string {\n return `${sub.kind}:${sub.identifier.toLowerCase()}`;\n}\n\n/**\n * Inverse of `subscriptionToSourceKey`: parse a `kind:identifier` source key\n * back into a subscription. Returns null for unrecognized keys (older rows\n * from a previous plugin version, manual edits, etc.) so the caller can log\n * and skip without crashing the whole poll.\n *\n * Note: the identifier coming back is lower-cased (per the source key\n * convention). Nyaa is case-insensitive on usernames and search terms, so\n * the round-trip is lossless for our purposes.\n */\nexport function sourceKeyToSubscription(key: string): UploaderSubscription | null {\n const idx = key.indexOf(\":\");\n if (idx <= 0 || idx === key.length - 1) return null;\n const kind = key.slice(0, idx);\n const identifier = key.slice(idx + 1);\n if (kind === \"user\" || kind === \"query\" || kind === \"params\") {\n return { kind, identifier };\n }\n return null;\n}\n\n/**\n * Parse the admin `uploaders` config into a clean list of subscriptions.\n *\n * Accepts either a JSON array (preferred \u2014 what the manifest now declares) or\n * a legacy comma-separated string. The string path is retained so existing\n * stored configs and CLI/env-driven setups keep working without a migration.\n *\n * Skips empty tokens; preserves order; deduplicates case-insensitively.\n */\nexport function parseSubscriptionList(raw: unknown): UploaderSubscription[] {\n let tokens: string[];\n if (Array.isArray(raw)) {\n tokens = raw.filter((t): t is string => typeof t === \"string\");\n } else if (typeof raw === \"string\") {\n tokens = raw.split(\",\");\n } else {\n return [];\n }\n\n const seen = new Set<string>();\n const out: UploaderSubscription[] = [];\n for (const token of tokens) {\n const sub = parseSubscriptionToken(token);\n if (sub === null) continue;\n const key = subscriptionToSourceKey(sub);\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(sub);\n }\n return out;\n}\n\n/** Build the per-subscription RSS URL. */\nexport function feedUrl(\n subscription: UploaderSubscription,\n baseUrl: string = NYAA_BASE_URL,\n): string {\n const base = baseUrl.replace(/\\/+$/, \"\");\n if (subscription.kind === \"user\") {\n return `${base}/?page=rss&u=${encodeURIComponent(subscription.identifier)}`;\n }\n if (subscription.kind === \"query\") {\n return `${base}/?page=rss&q=${encodeURIComponent(subscription.identifier)}`;\n }\n // params: identifier is already a URL-encoded, allowlisted query string.\n return `${base}/?page=rss&${subscription.identifier}`;\n}\n\n/**\n * Conditional GET against an uploader-subscription RSS feed.\n *\n * @param subscription - The uploader subscription to fetch.\n * @param previousEtag - The ETag from the previous successful poll (if any).\n * @param previousLastModified - Optional Last-Modified header from the previous\n * poll. Nyaa often returns one but doesn't always honor `If-None-Match`;\n * sending both maximizes 304 hit rate.\n * @param opts - Fetcher options (custom fetch, timeout, base URL override).\n */\nexport async function fetchSubscriptionFeed(\n subscription: UploaderSubscription,\n previousEtag: string | null,\n previousLastModified: string | null,\n opts: FetcherOptions = {},\n): Promise<FetchResult> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n const timeoutMs = opts.timeoutMs ?? 10_000;\n const baseUrl = opts.baseUrl ?? NYAA_BASE_URL;\n\n const url = feedUrl(subscription, baseUrl);\n const headers: Record<string, string> = {\n Accept: \"application/rss+xml, application/xml;q=0.9, */*;q=0.5\",\n \"User-Agent\": \"Codex-ReleaseTracker/1.0 (+https://github.com/AshDevFr/codex)\",\n };\n if (previousEtag) {\n headers[\"If-None-Match\"] = previousEtag;\n }\n if (previousLastModified) {\n headers[\"If-Modified-Since\"] = previousLastModified;\n }\n\n const signal = AbortSignal.timeout(timeoutMs);\n\n let resp: Response;\n try {\n resp = await fetchImpl(url, { method: \"GET\", headers, signal });\n } catch (err) {\n const msg = err instanceof Error ? err.message : \"Unknown fetch error\";\n return { kind: \"error\", status: 0, message: msg };\n }\n\n if (resp.status === 304) {\n return { kind: \"notModified\", status: 304 };\n }\n\n if (resp.status === 200) {\n const body = await resp.text();\n const etag = resp.headers.get(\"etag\");\n const lastModified = resp.headers.get(\"last-modified\");\n return { kind: \"ok\", body, etag, lastModified, status: 200 };\n }\n\n return {\n kind: \"error\",\n status: resp.status,\n message: `upstream returned ${resp.status} ${resp.statusText}`,\n };\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-release-nyaa\",\n \"version\": \"1.21.1\",\n \"description\": \"Nyaa.si uploader-feed release-source plugin for Codex - announces torrent releases for tracked series, filtered by an admin allowlist of trusted uploaders\",\n \"main\": \"dist/index.js\",\n \"bin\": \"dist/index.js\",\n \"type\": \"module\",\n \"files\": [\n \"dist\",\n \"README.md\"\n ],\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/AshDevFr/codex.git\",\n \"directory\": \"plugins/release-nyaa\"\n },\n \"scripts\": {\n \"build\": \"esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.js --sourcemap --banner:js='#!/usr/bin/env node'\",\n \"dev\": \"npm run build -- --watch\",\n \"clean\": \"rm -rf dist\",\n \"start\": \"node dist/index.js\",\n \"lint\": \"biome check .\",\n \"lint:fix\": \"biome check --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run --passWithNoTests\",\n \"test:watch\": \"vitest\",\n \"prepublishOnly\": \"npm run lint && npm run build\"\n },\n \"keywords\": [\n \"codex\",\n \"plugin\",\n \"nyaa\",\n \"release-source\",\n \"manga\",\n \"torrent\"\n ],\n \"author\": \"Codex\",\n \"license\": \"MIT\",\n \"engines\": {\n \"node\": \">=22.0.0\"\n },\n \"dependencies\": {\n \"@ashdev/codex-plugin-sdk\": \"^1.21.1\"\n },\n \"devDependencies\": {\n \"@biomejs/biome\": \"^2.4.4\",\n \"@types/node\": \"^22.0.0\",\n \"esbuild\": \"^0.27.3\",\n \"typescript\": \"^5.9.3\",\n \"vitest\": \"^4.0.18\"\n }\n}\n", "import type { PluginManifest } from \"@ashdev/codex-plugin-sdk\";\nimport packageJson from \"../package.json\" with { type: \"json\" };\n\n/** Default per-fetch HTTP timeout. Nyaa is usually fast; 10s is generous. */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 10_000;\n\n/**\n * Default minimum confidence threshold for emitted candidates. Nyaa matches\n * series via title parsing + alias comparison, which is fuzzier than the\n * external-ID match used by MangaUpdates. The host's threshold (default 0.7)\n * still filters at record time; this is the plugin-side floor below which we\n * don't even bother calling `releases/record`.\n */\nexport const DEFAULT_MIN_CONFIDENCE = 0.7;\n\nexport const manifest = {\n name: \"release-nyaa\",\n displayName: \"Nyaa Releases\",\n version: packageJson.version,\n description:\n \"Announces new chapter / volume torrents for tracked series via Nyaa.si uploader RSS feeds. Limited to an admin-configured uploader allowlist; matches via title aliases.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n releaseSource: {\n kinds: [\"rss-uploader\"],\n requiresAliases: true,\n canAnnounceChapters: true,\n canAnnounceVolumes: true,\n },\n },\n configSchema: {\n description:\n \"Nyaa plugin configuration. The plugin polls the listed uploaders' RSS feeds (or, for groups without a Nyaa account, a fallback search query) and emits release candidates only for tracked series whose aliases match the parsed title. Notification-only: Codex never downloads torrents.\",\n fields: [\n {\n key: \"uploaders\",\n label: \"Uploader Subscriptions\",\n description:\n \"List of trusted uploader handles or queries. Each entry is one of: `username` (a Nyaa user feed); `q:<query>` (a plain site-wide search); or `q:?<params>` (URL-style allowlisted params: `q`, `c`, `f`, `u` \u2014 e.g. `q:?c=3_1&q=Berserk` to search the Literature \u2192 English-translated category). Accepts a JSON array (preferred) or a legacy comma-separated string. Confidence stays above the rejection threshold only for entries that match a tracked series alias.\",\n type: \"string-array\" as const,\n required: false,\n default: [],\n example: [\"1r0n\", \"TankobonBlur\", \"q:LuminousScans\", \"q:?c=3_1&q=Berserk\"],\n },\n {\n key: \"requestTimeoutMs\",\n label: \"Request Timeout (ms)\",\n description:\n \"How long to wait for a single Nyaa RSS fetch before giving up. Defaults to 10000 (10 seconds).\",\n type: \"number\" as const,\n required: false,\n default: DEFAULT_REQUEST_TIMEOUT_MS,\n },\n {\n key: \"baseUrl\",\n label: \"Nyaa Base URL\",\n description:\n \"Override the Nyaa base URL. Useful for mirrors or for tests. Defaults to https://nyaa.si.\",\n type: \"string\" as const,\n required: false,\n default: \"https://nyaa.si\",\n example: \"https://nyaa.si\",\n },\n ],\n },\n userDescription:\n \"Watches Nyaa.si uploader feeds for new releases of tracked series. Matches by title alias \u2014 make sure your series' aliases (auto-populated from metadata or added manually in the Tracking panel) cover the way the uploader names them. Notification-only \u2014 Codex never downloads anything.\",\n adminSetupInstructions:\n \"1. Set the **Uploaders** config field to a JSON array of entries (a comma-separated string is still accepted for backwards compatibility). Each entry is one of: `username` (a Nyaa user feed, e.g. `tsuna69`), `q:<query>` (a plain site-wide search, e.g. `q:LuminousScans`), or `q:?<params>` (URL-style search with allowlisted keys `q`, `c`, `f`, `u`, e.g. `q:?c=3_1&q=Berserk` for the English-translated Literature category). 2. Save. The plugin restarts and the host materializes one row per entry in **Settings \u2192 Release tracking** \u2014 that's where you flip rows on/off, override the poll interval, or hit *Poll now*. 3. Make sure tracked series have aliases that match how the uploader names releases (alternate spellings, romanizations, volume-range tags). The plugin auto-prunes rows when you remove an entry from the list and re-save, so the Release tracking table stays in sync with this list.\",\n} as const satisfies PluginManifest & {\n capabilities: { releaseSource: { kinds: [\"rss-uploader\"] } };\n};\n", "/**\n * Alias matcher for Nyaa releases.\n *\n * Nyaa identifies series only by name in the torrent title \u2014 there's no\n * `nyaa_id` or other stable external ID that ties a release to a specific\n * series in our DB. So matching is a two-step pipeline:\n *\n * 1. Normalize the parsed `seriesGuess` and every alias the host returned\n * to a common shape (lowercase, alphanumeric + spaces only). This\n * mirrors the `normalize_alias` function on the host\n * ([src/db/entities/series_aliases.rs](src/db/entities/series_aliases.rs))\n * so a release whose normalized title exactly matches one of a series'\n * stored aliases lands at confidence 0.95.\n * 2. If no exact match, compute a token-level S\u00F8rensen-Dice similarity\n * against every candidate alias. The highest ratio wins, scaled into a\n * 0.7..0.85 confidence band; below the configured threshold we skip.\n *\n * The Dice ratio is more forgiving than edit distance for word-rearranged\n * titles (`\"Boruto Two Blue Vortex\"` vs. `\"Boruto - Two Blue Vortex\"`) while\n * still rejecting unrelated series at the threshold. We deliberately don't\n * wire a heavy fuzzy-match library; the surface area is small.\n */\n\n/** A tracked-series candidate with its raw aliases. */\nexport interface AliasCandidate {\n /** Codex series UUID. */\n seriesId: string;\n /** Raw aliases from `releases/list_tracked`. */\n aliases: string[];\n}\n\n/** A successful match. */\nexport interface AliasMatch {\n seriesId: string;\n confidence: number;\n /** Reason string surfaced in the SeriesMatch \u2014 \"alias-exact\" or \"alias-fuzzy\". */\n reason: string;\n /** The matched alias (raw form, for logging). */\n matchedAlias: string;\n}\n\n/**\n * Confidence assigned on an exact normalized match.\n *\n * Below 1.0 because we still don't have an external ID \u2014 a release titled\n * `\"X\"` could legitimately match multiple series with that alias. The host's\n * threshold treats this as a strong-but-not-certain signal.\n */\nexport const CONFIDENCE_EXACT = 0.95;\n\n/**\n * Floor below which fuzzy matches don't get emitted. The host's default\n * threshold is 0.7; we share that floor so plugin-side filtering doesn't\n * silently second-guess host config.\n */\nexport const DEFAULT_FUZZY_FLOOR = 0.7;\n\n/**\n * Anything below this Dice-coefficient is rejected outright (even before the\n * confidence floor kicks in). 0.85 lets through \"two-blue-vortex\" vs. \"two\n * blue vortex\" but kills \"naruto\" vs. \"boruto two blue vortex\".\n */\nexport const MIN_DICE_RATIO = 0.85;\n\n// ---------------------------------------------------------------------------\n// Normalization\n// ---------------------------------------------------------------------------\n\n/**\n * Normalize an alias to the same shape the host stores in\n * `series_aliases.normalized`. Mirrors the Rust `normalize_alias` impl \u2014 keep\n * these in lockstep.\n */\nexport function normalizeAlias(input: string): string {\n let out = \"\";\n let lastWasSpace = false;\n for (const ch of input) {\n // Match Rust's `is_alphanumeric()` (Unicode-aware).\n if (/[\\p{L}\\p{N}]/u.test(ch)) {\n out += ch.toLowerCase();\n lastWasSpace = false;\n } else if (/\\s/.test(ch) && out.length > 0 && !lastWasSpace) {\n out += \" \";\n lastWasSpace = true;\n }\n // Anything else (punctuation, control, symbols) is dropped.\n }\n return out.endsWith(\" \") ? out.slice(0, -1) : out;\n}\n\n// ---------------------------------------------------------------------------\n// Dice coefficient (token-level, character-bigram fallback)\n// ---------------------------------------------------------------------------\n\n/**\n * S\u00F8rensen-Dice coefficient on word-bigrams of the input strings (with a\n * character-bigram fallback for short / single-word strings).\n *\n * Range: 0..1, where 1.0 means identical bigram sets.\n */\nexport function diceRatio(a: string, b: string): number {\n if (a.length === 0 || b.length === 0) return 0;\n if (a === b) return 1;\n\n const bigramsA = bigrams(a);\n const bigramsB = bigrams(b);\n if (bigramsA.size === 0 || bigramsB.size === 0) return 0;\n\n let intersection = 0;\n for (const bg of bigramsA) {\n if (bigramsB.has(bg)) intersection++;\n }\n return (2 * intersection) / (bigramsA.size + bigramsB.size);\n}\n\nfunction bigrams(s: string): Set<string> {\n const out = new Set<string>();\n // Word bigrams first.\n const words = s.split(/\\s+/).filter((w) => w.length > 0);\n if (words.length >= 2) {\n for (let i = 0; i < words.length - 1; i++) {\n out.add(`${words[i]} ${words[i + 1]}`);\n }\n }\n // Plus character bigrams to handle word-rearrangement and short strings.\n const flat = s.replace(/\\s+/g, \"\");\n if (flat.length >= 2) {\n for (let i = 0; i < flat.length - 1; i++) {\n out.add(`#${flat.slice(i, i + 2)}`);\n }\n } else if (flat.length === 1) {\n out.add(`#${flat}`);\n }\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Public matching entry point\n// ---------------------------------------------------------------------------\n\nexport interface MatchOptions {\n /**\n * Minimum confidence for a fuzzy match to be returned. Defaults to\n * `DEFAULT_FUZZY_FLOOR` (0.7). Below this, the matcher returns null.\n */\n fuzzyFloor?: number;\n}\n\n/**\n * Match a parsed series-guess against a list of tracked-series candidates and\n * their aliases. Returns the best match or null if nothing clears the floor.\n *\n * On an exact normalized match against any alias of a candidate, confidence\n * is `CONFIDENCE_EXACT` (0.95). If multiple candidates have aliases that\n * normalize to the same form, the first one wins \u2014 that's a data-quality\n * issue the host surfaces via the `latest_known_*` advance gate, not\n * something the matcher can untangle alone.\n *\n * On no exact match, the matcher computes Dice ratios across the cartesian\n * product (candidates \u00D7 aliases), finds the maximum, scales it from\n * `[MIN_DICE_RATIO, 1.0]` into `[fuzzyFloor, 0.85]`, and returns a fuzzy\n * match if the result is at or above the floor.\n */\nexport function matchSeries(\n seriesGuess: string,\n candidates: AliasCandidate[],\n opts: MatchOptions = {},\n): AliasMatch | null {\n const floor = opts.fuzzyFloor ?? DEFAULT_FUZZY_FLOOR;\n const target = normalizeAlias(seriesGuess);\n if (target.length === 0 || candidates.length === 0) return null;\n\n // Pass 1 \u2014 exact normalized match.\n for (const c of candidates) {\n for (const alias of c.aliases) {\n if (normalizeAlias(alias) === target) {\n return {\n seriesId: c.seriesId,\n confidence: CONFIDENCE_EXACT,\n reason: \"alias-exact\",\n matchedAlias: alias,\n };\n }\n }\n }\n\n // Pass 2 \u2014 best fuzzy match.\n let best: AliasMatch | null = null;\n let bestRatio = 0;\n for (const c of candidates) {\n for (const alias of c.aliases) {\n const ratio = diceRatio(target, normalizeAlias(alias));\n if (ratio > bestRatio) {\n bestRatio = ratio;\n best = {\n seriesId: c.seriesId,\n confidence: 0,\n reason: \"alias-fuzzy\",\n matchedAlias: alias,\n };\n }\n }\n }\n if (best === null || bestRatio < MIN_DICE_RATIO) return null;\n\n // Linearly scale [MIN_DICE_RATIO..1.0] \u2192 [fuzzyFloor..0.85].\n // (We cap the fuzzy ceiling below CONFIDENCE_EXACT so an alias-exact match\n // is always strictly stronger than the best alias-fuzzy match.)\n const ceiling = 0.85;\n const span = 1 - MIN_DICE_RATIO;\n const t = (bestRatio - MIN_DICE_RATIO) / span; // 0..1 inside the band\n const confidence = floor + t * (ceiling - floor);\n if (confidence < floor) return null;\n best.confidence = Number(confidence.toFixed(4));\n return best;\n}\n\n/**\n * Match a list of alias guesses (e.g. from a `Title A / Title B` Nyaa title)\n * and return the best result across them.\n *\n * Picks the highest-confidence match across all guesses, preferring\n * `alias-exact` over `alias-fuzzy` when ties exist (because exact carries a\n * fixed `CONFIDENCE_EXACT` and fuzzy is bounded below it). When two guesses\n * both produce alias-exact matches against different series, the first guess\n * wins \u2014 that's the same precedence rule `matchSeries` applies internally\n * across candidates.\n */\nexport function matchSeriesAny(\n seriesGuesses: string[],\n candidates: AliasCandidate[],\n opts: MatchOptions = {},\n): AliasMatch | null {\n if (seriesGuesses.length === 0) return null;\n let best: AliasMatch | null = null;\n for (const guess of seriesGuesses) {\n const m = matchSeries(guess, candidates, opts);\n if (m === null) continue;\n if (best === null || m.confidence > best.confidence) {\n best = m;\n }\n }\n return best;\n}\n", "/**\n * RSS parser for Nyaa.si feeds.\n *\n * Nyaa's RSS namespace exposes one extra element per item that we care about\n * (`<nyaa:infoHash>`), plus the standard `<title>`, `<link>`, `<guid>`,\n * `<pubDate>`, and `<description>` fields. We pull all of them with the same\n * lightweight regex pipeline used for MangaUpdates \u2014 no heavy XML dep.\n *\n * Parsing the title is where most of the work is. Real-world examples\n * (sourced from production Nyaa feeds and the user's screenshot of 1r0n's\n * subscription):\n *\n * \"[1r0n] Boruto - Two Blue Vortex - Volume 02 (Digital) (1r0n)\"\n * \"[1r0n] One Piece v107 (Digital)\"\n * \"[1r0n] Chainsaw Man - Chapter 142 (Digital)\"\n * \"[Group] Dandadan c126-142 (2024) (Digital)\"\n * \"[Tankobon Blur] Solo Leveling Vol. 13 (2024) (Digital) (Tankobon Blur)\"\n * \"Berserk Volume 42 (Digital)\"\n *\n * The shape we want out of each item:\n * - parsed series guess (alias-free string used for matching)\n * - chapter / volume axes (decimals supported on chapter)\n * - format hints (Digital / JXL / etc.)\n * - uploader-tagged group (if encoded as a leading `[Group]` token)\n *\n * Nyaa titles are noisy; we keep parsing best-effort and surface confidence\n * downstream from the alias matcher rather than failing here.\n */\n\n/**\n * Numeric inclusive range. Single values are encoded as `start === end`\n * (`{ start: 5, end: 5 }`).\n */\nexport interface NumericSpan {\n start: number;\n end: number;\n}\n\n/** Parsed item, pre-`ReleaseCandidate`. */\nexport interface ParsedRssItem {\n /** Stable per-source ID. Derived from the link or guid. */\n externalReleaseId: string;\n /** Original title. Useful for debugging / fallback. */\n title: string;\n /** Series-name guess after stripping volume/chapter/group/format tokens. */\n seriesGuess: string;\n /**\n * All alias candidates extracted from the series-name region. When the title\n * uses `Title A / Title B` (a common 1r0n / LuCaZ convention for \"JP name /\n * EN name\"), both halves are surfaced here so the matcher can score against\n * either. For titles without a slash separator this is a single-element\n * array equal to `[seriesGuess]`.\n */\n seriesGuessAliases: string[];\n /**\n * Volume coverage as a normalized span list. Sorted ascending by `start`,\n * with overlapping spans merged. Disjoint spans (`v01-04 + v06-09`) are\n * preserved as two entries. `null` when the title carried no volume\n * information at all.\n */\n volumes: NumericSpan[] | null;\n /**\n * Chapter coverage as a normalized span list. Same conventions as\n * [`volumes`]; decimals are preserved (`c12.5` \u2192 `[{12.5, 12.5}]`).\n */\n chapters: NumericSpan[] | null;\n /** Leading `[Group]` token, if any. */\n group: string | null;\n /** Format hints as a small dictionary (digital, jxl, ...). */\n formatHints: Record<string, boolean>;\n /** RSS `<link>` value. On Nyaa this is the `.torrent` download URL. */\n link: string;\n /**\n * Permalink to the release post page (e.g. `https://nyaa.si/view/12345`),\n * derived from the `<guid isPermaLink=\"true\">` tag. Null when the guid is\n * missing or doesn't look like a post URL.\n */\n pageUrl: string | null;\n /** `nyaa:infoHash` value, lowercased; null if missing. */\n infoHash: string | null;\n /** ISO-8601 timestamp. Falls back to \"now\" if pubDate is missing/invalid. */\n observedAt: string;\n}\n\n// -----------------------------------------------------------------------------\n// XML helpers (mirror release-mangaupdates conventions)\n// -----------------------------------------------------------------------------\n\nfunction decodeXmlText(raw: string): string {\n let s = raw.trim();\n const cdataMatch = s.match(/^<!\\[CDATA\\[([\\s\\S]*?)]]>$/);\n if (cdataMatch?.[1] !== undefined) {\n s = cdataMatch[1];\n }\n return s\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&apos;/g, \"'\");\n}\n\n/** Pull the first `<tag>` text content from an XML fragment, or null. */\nfunction extractTagText(xml: string, tag: string): string | null {\n // Escape `:` for namespaced tags (e.g. `nyaa:infoHash`).\n const safeTag = tag.replace(/:/g, \"\\\\:\");\n const re = new RegExp(`<${safeTag}[^>]*>([\\\\s\\\\S]*?)</${safeTag}>`, \"i\");\n const m = xml.match(re);\n if (!m?.[1]) return null;\n return decodeXmlText(m[1]);\n}\n\nfunction splitItems(xml: string): string[] {\n const out: string[] = [];\n const re = /<item\\b[^>]*>([\\s\\S]*?)<\\/item>/gi;\n for (;;) {\n const match = re.exec(xml);\n if (match === null) break;\n if (match[1] !== undefined) out.push(match[1]);\n }\n return out;\n}\n\n// -----------------------------------------------------------------------------\n// Title parsing\n// -----------------------------------------------------------------------------\n\n/**\n * Strip a leading `[Group]` token off the title and return both pieces.\n * If the title has no leading bracketed token, returns `{ rest: title,\n * group: null }`.\n */\nfunction extractLeadingGroup(title: string): { rest: string; group: string | null } {\n const m = title.match(/^\\s*\\[([^\\]]+)\\]\\s*(.*)$/);\n if (!m?.[1]) return { rest: title, group: null };\n const group = m[1].trim();\n const rest = m[2] ?? \"\";\n return { rest, group: group.length > 0 ? group : null };\n}\n\n/**\n * Strip every `(...)` group from a string. Used to keep year ranges, uploader\n * credits, and format-hint tags out of the chapter/volume tokenizer \u2014 those\n * always live inside parentheses, so anything inside them must not be\n * interpreted as release-info.\n */\nfunction stripParens(s: string): string {\n return s.replace(/\\([^)]*\\)/g, \" \");\n}\n\n/**\n * Locate the start of the \"release-info span\" \u2014 the offset in `s` (which has\n * already had `(...)` groups blanked) where chapter/volume tokens begin.\n *\n * Anchors, in priority order:\n * 1. A `v##`, `vol.##`, `volume ##` token (with or without a range).\n * 2. A bare numeric range with both sides at 3+ digits (`031-037`,\n * `001-069`). Two-digit forms are rejected to avoid false positives\n * inside series names (`30s`, `My 100`, etc.).\n * 3. A `c##` / `ch.##` / `Chapter ##` token.\n *\n * Returns the index of the anchor, or -1 if no release-info is present (the\n * whole string is then treated as a series name).\n */\nfunction findReleaseInfoStart(s: string): number {\n const anchors: RegExp[] = [\n /\\b(?:v|vol|volume)\\.?\\s*[0-9]+/i,\n /\\b[0-9]{3,4}\\s*[-\u2013]\\s*[0-9]{3,4}\\b/,\n /\\b(?:c|ch|chapter)\\.?\\s*[0-9]+/i,\n ];\n let best = -1;\n for (const re of anchors) {\n const m = s.match(re);\n if (m && m.index !== undefined && (best === -1 || m.index < best)) {\n best = m.index;\n }\n }\n return best;\n}\n\n/**\n * Spread tokens are the comma- / `+`- / whitespace- / `as`-separated atoms\n * that make up the release-info span:\n *\n * - `volume` : single volume number (`v01`, `Vol. 13`)\n * - `volRange` : volume range (`v01-14`)\n * - `chapter` : single chapter number (`c143`, bare `70`)\n * - `chapRange` : chapter range (`c126-142`, bare `031-037`)\n *\n * The tokenizer scans left-to-right and consumes one token per match. Bare\n * numeric tokens are only accepted *after* the release-info anchor \u2014 see\n * `findReleaseInfoStart` \u2014 so series-name digits don't leak in.\n */\ntype SpreadToken =\n | { kind: \"volume\"; value: number }\n | { kind: \"volRange\"; start: number; end: number }\n | { kind: \"chapter\"; value: number }\n | { kind: \"chapRange\"; start: number; end: number };\n\n/**\n * Tokenize the release-info span into volume/chapter atoms.\n *\n * `s` should be the parens-stripped substring starting at the release-info\n * anchor. The tokenizer is intentionally permissive about separators (commas,\n * `+`, whitespace, `as`) \u2014 we just consume tokens greedily and aggregate\n * downstream.\n */\nfunction tokenizeReleaseInfo(s: string): SpreadToken[] {\n const tokens: SpreadToken[] = [];\n\n // Match either a prefixed volume/chapter token, or a bare numeric range /\n // single. The order in the alternation matters: ranges must be tried before\n // single tokens, and prefixed forms must be tried before bare numerics so\n // we don't mis-classify `v05` as bare-chapter `5`.\n //\n // 1. `v##-##` / `vol.##-##` / `volume ##-##` \u2192 volRange\n // 2. `v##` / `vol.##` / `volume ##` \u2192 volume\n // 3. `c##.##-##.##` / `ch.##-##` / `Chapter ##-##` \u2192 chapRange\n // 4. `c##.##` / `ch.##` / `Chapter ##` \u2192 chapter\n // 5. bare `###-###` (3+ digits each side) \u2192 chapRange\n // 6. bare `##` (1+ digits) \u2014 only matches *after* the first anchor token\n // has been emitted, see `acceptShortBare` below. Lets us pick up\n // \"extra\" chapters expressed as short numerics (`+ 70`) without\n // promoting incidental name-region digits.\n const tokenRe = new RegExp(\n [\n \"\\\\b(?<vrs>v|vol|volume)\\\\.?\\\\s*([0-9]+)\\\\s*[-\u2013]\\\\s*([0-9]+)\\\\b\",\n \"\\\\b(?<vss>v|vol|volume)\\\\.?\\\\s*([0-9]+)\\\\b\",\n \"\\\\b(?<crs>c|ch|chapter)\\\\.?\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\s*[-\u2013]\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\b\",\n \"\\\\b(?<css>c|ch|chapter)\\\\.?\\\\s*([0-9]+(?:\\\\.[0-9]+)?)\\\\b\",\n \"\\\\b(?<brs>)([0-9]{3,4})\\\\s*[-\u2013]\\\\s*([0-9]{3,4})\\\\b\",\n \"\\\\b(?<bss>)([0-9]{1,4})\\\\b\",\n ].join(\"|\"),\n \"gi\",\n );\n\n for (;;) {\n const m = tokenRe.exec(s);\n if (m === null) break;\n const groups = m.groups ?? {};\n if (groups.vrs !== undefined) {\n const start = Number.parseInt(m[2] ?? \"\", 10);\n const end = Number.parseInt(m[3] ?? \"\", 10);\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"volRange\", start, end });\n }\n continue;\n }\n if (groups.vss !== undefined) {\n const value = Number.parseInt(m[5] ?? \"\", 10);\n if (Number.isFinite(value)) tokens.push({ kind: \"volume\", value });\n continue;\n }\n if (groups.crs !== undefined) {\n const start = Number.parseFloat(m[7] ?? \"\");\n const end = Number.parseFloat(m[8] ?? \"\");\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"chapRange\", start, end });\n }\n continue;\n }\n if (groups.css !== undefined) {\n const value = Number.parseFloat(m[10] ?? \"\");\n if (Number.isFinite(value)) tokens.push({ kind: \"chapter\", value });\n continue;\n }\n if (groups.brs !== undefined) {\n const start = Number.parseInt(m[12] ?? \"\", 10);\n const end = Number.parseInt(m[13] ?? \"\", 10);\n if (Number.isFinite(start) && Number.isFinite(end)) {\n tokens.push({ kind: \"chapRange\", start, end });\n }\n continue;\n }\n if (groups.bss !== undefined) {\n const raw = m[15] ?? \"\";\n const value = Number.parseInt(raw, 10);\n if (!Number.isFinite(value)) continue;\n // Only accept short (\u22642 digit) bare numerics once we've already\n // committed to a richer token; on its own a `42` is more likely a\n // year fragment or noise than a chapter. 3+ digits is unambiguous in\n // this corpus so we always accept it.\n if (raw.length < 3 && tokens.length === 0) continue;\n tokens.push({ kind: \"chapter\", value });\n }\n }\n\n return tokens;\n}\n\n/**\n * Project the spread-token list onto two axes of normalized [`NumericSpan`]s.\n *\n * Single-value tokens (`v05`, `c143`) become `{ start: N, end: N }`. Range\n * tokens (`v01-09`, `001-050`, `c126-142`) become `{ start, end }` with the\n * lower number on `start`. We do *not* min/max aggregate across an axis any\n * more \u2014 disjoint ranges (`v01-04 + v06-09`) must survive intact so the host\n * can ask \"does the user own everything in here?\" honestly.\n */\nfunction tokensToSpans(tokens: SpreadToken[]): {\n volumes: NumericSpan[] | null;\n chapters: NumericSpan[] | null;\n} {\n const vol: NumericSpan[] = [];\n const chap: NumericSpan[] = [];\n for (const t of tokens) {\n switch (t.kind) {\n case \"volume\":\n vol.push({ start: t.value, end: t.value });\n break;\n case \"volRange\":\n vol.push({ start: t.start, end: t.end });\n break;\n case \"chapter\":\n chap.push({ start: t.value, end: t.value });\n break;\n case \"chapRange\":\n chap.push({ start: t.start, end: t.end });\n break;\n }\n }\n return {\n volumes: vol.length === 0 ? null : normalizeSpans(vol),\n chapters: chap.length === 0 ? null : normalizeSpans(chap),\n };\n}\n\n/**\n * Sort a span list ascending and merge overlapping entries. We deliberately\n * do *not* merge purely adjacent spans (`[1, 4]` and `[5, 9]` stay separate);\n * the uploader chose to write them disjointly and we preserve that intent.\n * Bad inputs where `start > end` get swapped before sorting so downstream\n * iteration always sees `start <= end`.\n */\nfunction normalizeSpans(spans: NumericSpan[]): NumericSpan[] {\n if (spans.length === 0) return spans;\n const fixed = spans.map((s) =>\n s.start <= s.end ? { start: s.start, end: s.end } : { start: s.end, end: s.start },\n );\n fixed.sort((a, b) => a.start - b.start || a.end - b.end);\n const out: NumericSpan[] = [];\n for (const s of fixed) {\n const last = out[out.length - 1];\n if (last !== undefined && s.start <= last.end) {\n // Overlap \u2014 extend the existing span. Equality of endpoints\n // (`[1, 4]` + `[4, 9]`) counts as overlap too.\n if (s.end > last.end) last.end = s.end;\n } else {\n out.push(s);\n }\n }\n return out;\n}\n\n/**\n * Walk the parenthesized tags in the title and extract format hints.\n *\n * Common Nyaa hints we care about:\n * - `(Digital)` \u2192 `digital`\n * - `(JXL)` \u2192 `jxl`\n * - `(Mag-Z)` / `(Magazine)` \u2192 `magazine`\n * - `(Omnibus Edition)` / `(Omnibus)` \u2192 `omnibus`\n * - `(2024)` is a year, ignored (we'd need it for naming dedup but not for filtering)\n */\nfunction extractFormatHints(s: string): Record<string, boolean> {\n const hints: Record<string, boolean> = {};\n const tagRe = /\\(([^)]+)\\)/g;\n for (;;) {\n const match = tagRe.exec(s);\n if (match === null) break;\n const tag = (match[1] ?? \"\").trim().toLowerCase();\n if (tag.length === 0) continue;\n if (tag === \"digital\") hints.digital = true;\n else if (tag === \"jxl\") hints.jxl = true;\n else if (tag === \"magazine\" || tag === \"mag-z\") hints.magazine = true;\n else if (tag === \"webtoon\") hints.webtoon = true;\n else if (tag === \"bw\" || tag === \"b&w\") hints.bw = true;\n else if (tag === \"color\") hints.color = true;\n else if (tag === \"omnibus\" || tag === \"omnibus edition\") hints.omnibus = true;\n }\n return hints;\n}\n\n/**\n * Strip a trailing `[...]` token (e.g. `[Oak]` at the end of some\n * danke-Empire releases). Mirrors `extractLeadingGroup` but at the tail and\n * without surfacing the value \u2014 trailing brackets are credit, not a parsing\n * signal we currently use.\n */\nfunction stripTrailingBracket(s: string): string {\n return s.replace(/\\s*\\[[^\\]]+\\]\\s*$/g, \"\").trim();\n}\n\n/**\n * Take the \"name region\" of a release title (everything before the first\n * release-info anchor, with parens already stripped) and reduce it to a clean\n * primary guess plus alias candidates.\n *\n * The name region may still contain:\n * - subtitle dashes: `Boruto - Two Blue Vortex` \u2192 joined with spaces\n * - alias separator: `Ao no Hako / Blue Box` \u2192 both halves returned\n *\n * Apostrophes and hyphenated words (`Amagami-san`, `Chillin'`) are preserved\n * \u2014 the host's `normalize_alias` strips them at match time, but we want to\n * keep them readable in logs and admin surfaces.\n */\nfunction extractSeriesAliases(nameRegion: string): {\n primary: string;\n aliases: string[];\n} {\n // Subtitle dashes: ` - `, ` \u2013 `, ` \u2014 ` are titling glue, not separators.\n // Joining the halves with a single space mirrors the prior behavior the\n // existing tests assert (`Boruto Two Blue Vortex`).\n const dashJoined = nameRegion.replace(/\\s+[-\u2013\u2014]\\s+/g, \" \");\n\n // Alias separator. Only ` / ` (with whitespace on both sides) splits \u2014 bare\n // `/` survives so e.g. `AC/DC Tales` stays one alias.\n const parts = dashJoined\n .split(/\\s+\\/\\s+/)\n .map((p) => p.replace(/\\s+/g, \" \").trim())\n .filter((p) => p.length > 0);\n\n if (parts.length === 0) return { primary: \"\", aliases: [] };\n return { primary: parts[0] ?? \"\", aliases: parts };\n}\n\n/**\n * Public entry point \u2014 extract the structured fields from a single Nyaa\n * release title.\n *\n * Returns null only if the title is empty after trimming. Otherwise returns a\n * best-effort parse where the series guess may still be empty (e.g. for\n * meta-bundles without a leading series name); the matcher then drops those.\n */\nexport function parseTitle(title: string): {\n seriesGuess: string;\n seriesGuessAliases: string[];\n volumes: NumericSpan[] | null;\n chapters: NumericSpan[] | null;\n group: string | null;\n formatHints: Record<string, boolean>;\n} | null {\n const trimmed = title.trim();\n if (trimmed.length === 0) return null;\n\n const { rest, group } = extractLeadingGroup(trimmed);\n const formatHints = extractFormatHints(rest);\n\n // Blank out `(...)` groups so years and uploader credits can't be picked up\n // by the release-info tokenizer, then split into name region / release-info\n // region at the first chapter/volume anchor.\n const flattened = stripTrailingBracket(stripParens(rest));\n const anchor = findReleaseInfoStart(flattened);\n const nameRegion = anchor === -1 ? flattened : flattened.slice(0, anchor);\n const infoRegion = anchor === -1 ? \"\" : flattened.slice(anchor);\n\n const tokens = tokenizeReleaseInfo(infoRegion);\n const { volumes, chapters } = tokensToSpans(tokens);\n const { primary, aliases } = extractSeriesAliases(nameRegion);\n\n return {\n seriesGuess: primary,\n seriesGuessAliases: aliases.length > 0 ? aliases : [primary],\n volumes,\n chapters,\n group,\n formatHints,\n };\n}\n\n// -----------------------------------------------------------------------------\n// Item parsing\n// -----------------------------------------------------------------------------\n\nfunction pubDateToIso(raw: string | null): string {\n if (raw) {\n const d = new Date(raw);\n if (!Number.isNaN(d.getTime())) return d.toISOString();\n }\n return new Date().toISOString();\n}\n\n/**\n * Pull the post-page URL out of the guid when it looks like a Nyaa\n * `/view/<id>` permalink. The `<link>` tag in Nyaa feeds is the `.torrent`\n * download URL, which is not what we want to surface to users.\n */\nfunction derivePageUrl(guid: string | null): string | null {\n if (!guid) return null;\n const trimmed = guid.trim();\n if (trimmed.length === 0) return null;\n // Match http(s)://<host>/view/<id> with optional trailing slash / query.\n if (/^https?:\\/\\/[^/]+\\/view\\/[^/?#]+/i.test(trimmed)) return trimmed;\n return null;\n}\n\nfunction deriveExternalReleaseId(\n guid: string | null,\n link: string | null,\n infoHash: string | null,\n title: string,\n pubDate: string | null,\n): string {\n if (guid && guid.trim().length > 0) return guid.trim();\n if (link && link.trim().length > 0) return link.trim();\n if (infoHash && infoHash.length > 0) return `urn:btih:${infoHash}`;\n // Deterministic fallback: djb2-ish hash. Same algorithm MangaUpdates uses.\n const fallback = `${title}|${pubDate ?? \"\"}`;\n let h = 5381;\n for (let i = 0; i < fallback.length; i++) {\n h = ((h << 5) + h + fallback.charCodeAt(i)) | 0;\n }\n return `t:${(h >>> 0).toString(36)}`;\n}\n\n/**\n * Parse a single Nyaa `<item>` block. Returns null when the title is missing\n * (truly malformed entry).\n */\nexport function parseItem(itemXml: string): ParsedRssItem | null {\n const title = extractTagText(itemXml, \"title\");\n if (!title) return null;\n\n const link = extractTagText(itemXml, \"link\");\n const guid = extractTagText(itemXml, \"guid\");\n const pubDate = extractTagText(itemXml, \"pubDate\");\n const infoHashRaw = extractTagText(itemXml, \"nyaa:infoHash\");\n const infoHash = infoHashRaw ? infoHashRaw.toLowerCase().trim() : null;\n\n const parsedTitle = parseTitle(title);\n if (parsedTitle === null) return null;\n\n return {\n externalReleaseId: deriveExternalReleaseId(guid, link, infoHash, title, pubDate),\n title,\n seriesGuess: parsedTitle.seriesGuess,\n seriesGuessAliases: parsedTitle.seriesGuessAliases,\n volumes: parsedTitle.volumes,\n chapters: parsedTitle.chapters,\n group: parsedTitle.group,\n formatHints: parsedTitle.formatHints,\n link: link ?? \"\",\n pageUrl: derivePageUrl(guid),\n infoHash,\n observedAt: pubDateToIso(pubDate),\n };\n}\n\n/**\n * Parse a full Nyaa RSS feed body into structured items. Bad items (missing\n * title) are dropped silently \u2014 Nyaa feeds occasionally include broken entries\n * and we'd rather keep going than poison the whole poll.\n */\nexport function parseFeed(xml: string): ParsedRssItem[] {\n return splitItems(xml)\n .map(parseItem)\n .filter((i): i is ParsedRssItem => i !== null);\n}\n", "/**\n * Nyaa.si Release-Source Plugin for Codex.\n *\n * Polls Nyaa user / search RSS feeds for an admin-configured uploader\n * allowlist and announces new releases for tracked series. Matching is\n * alias-based: each parsed Nyaa title is normalized and compared to every\n * tracked series' alias list. Confidence is 0.95 on exact normalized match,\n * dropping to a fuzzy floor of 0.7 for near-matches; below that, the\n * candidate is silently dropped (the host's threshold would reject it\n * anyway).\n *\n * Source-row model:\n * - On `onInitialize` (which the host re-runs after every config save),\n * the plugin parses the admin's `uploaders` CSV and calls\n * `releases/register_sources` with one entry per subscription. The host\n * materializes one `release_sources` row per uploader, keyed on\n * `(plugin_id, sourceKey)` where `sourceKey` is `kind:identifier`\n * (e.g. `user:tsuna69`, `query:luminousscans`, `params:c=3_1&q=berserk`).\n * - The host scheduler fires one `releases/poll` task per source row, so\n * each uploader has its own poll cadence, ETag, and last-error status.\n *\n * Flow per `releases/poll`:\n * 1. Recover the subscription from `params.config.subscription` (or fall\n * back to parsing `params.sourceKey`).\n * 2. Pull tracked-series + aliases from the host\n * (`releases/list_tracked`).\n * 3. Conditional GET the RSS feed using `params.etag`.\n * 4. Parse each item; match against tracked aliases; emit a candidate via\n * `releases/record`.\n * 5. Return the new ETag and upstream status for the host's per-host\n * backoff layer.\n */\n\nimport {\n createLogger,\n createReleaseSourcePlugin,\n type HostRpcClient,\n HostRpcError,\n type InitializeParams,\n RELEASES_METHODS,\n type ReleaseCandidate,\n type ReleasePollRequest,\n type ReleasePollResponse,\n type TrackedSeriesEntry,\n} from \"@ashdev/codex-plugin-sdk\";\nimport {\n fetchSubscriptionFeed,\n parseSubscriptionList,\n sourceKeyToSubscription,\n subscriptionToSourceKey,\n type UploaderSubscription,\n} from \"./fetcher.js\";\nimport { DEFAULT_MIN_CONFIDENCE, DEFAULT_REQUEST_TIMEOUT_MS, manifest } from \"./manifest.js\";\nimport { type AliasCandidate, type AliasMatch, matchSeriesAny } from \"./matcher.js\";\nimport { type ParsedRssItem, parseFeed } from \"./parser.js\";\n\nconst logger = createLogger({ name: manifest.name, level: \"info\" });\n\n// =============================================================================\n// Plugin-level state (set during initialize)\n// =============================================================================\n\ninterface PluginState {\n hostRpc: HostRpcClient | null;\n /** Parsed admin uploader subscription list. */\n subscriptions: UploaderSubscription[];\n /** Hard timeout for upstream fetches. */\n requestTimeoutMs: number;\n /** Minimum confidence floor \u2014 passed to the matcher's `fuzzyFloor`. */\n minConfidence: number;\n /** Override base URL (for tests / mirrors). */\n baseUrl: string | null;\n}\n\nconst state: PluginState = {\n hostRpc: null,\n subscriptions: [],\n requestTimeoutMs: DEFAULT_REQUEST_TIMEOUT_MS,\n minConfidence: DEFAULT_MIN_CONFIDENCE,\n baseUrl: null,\n};\n\n/** Reset state. Exported for tests; not part of the plugin contract. */\nexport function _resetState(): void {\n state.hostRpc = null;\n state.subscriptions = [];\n state.requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS;\n state.minConfidence = DEFAULT_MIN_CONFIDENCE;\n state.baseUrl = null;\n}\n\n// =============================================================================\n// Reverse-RPC wrappers\n// =============================================================================\n\ninterface ListTrackedResponse {\n tracked: TrackedSeriesEntry[];\n nextOffset?: number;\n}\n\ninterface RecordResponse {\n ledgerId: string;\n deduped: boolean;\n}\n\nasync function listTracked(\n rpc: HostRpcClient,\n sourceId: string,\n offset: number,\n limit: number,\n): Promise<ListTrackedResponse> {\n return rpc.call<ListTrackedResponse>(RELEASES_METHODS.LIST_TRACKED, {\n sourceId,\n offset,\n limit,\n });\n}\n\nasync function recordCandidate(\n rpc: HostRpcClient,\n sourceId: string,\n candidate: ReleaseCandidate,\n): Promise<RecordResponse | null> {\n try {\n return await rpc.call<RecordResponse>(RELEASES_METHODS.RECORD, {\n sourceId,\n candidate,\n });\n } catch (err) {\n if (err instanceof HostRpcError) {\n logger.warn(\n `record failed for ${candidate.externalReleaseId}: ${err.message} (code ${err.code})`,\n );\n } else {\n const msg = err instanceof Error ? err.message : \"unknown error\";\n logger.warn(`record failed for ${candidate.externalReleaseId}: ${msg}`);\n }\n return null;\n }\n}\n\n// =============================================================================\n// Iteration helpers\n// =============================================================================\n\n/**\n * Pull every tracked-series page from the host. We can't stream\n * subscription-by-subscription because each Nyaa item has to be matched\n * against the *full* alias set; partial pages would leak misses.\n */\nexport async function fetchAllTracked(\n rpc: HostRpcClient,\n sourceId: string,\n): Promise<AliasCandidate[]> {\n const out: AliasCandidate[] = [];\n const pageSize = 200;\n let offset = 0;\n while (true) {\n const page = await listTracked(rpc, sourceId, offset, pageSize);\n for (const entry of page.tracked) {\n const aliases = entry.aliases ?? [];\n // Drop entries with no aliases \u2014 Nyaa matching is alias-only.\n if (aliases.length === 0) continue;\n out.push({ seriesId: entry.seriesId, aliases });\n }\n if (page.nextOffset === undefined || page.tracked.length === 0) return out;\n offset = page.nextOffset;\n }\n}\n\n// =============================================================================\n// Per-subscription poll\n// =============================================================================\n\n/** Outcome of a single per-subscription fetch+parse cycle. */\nexport interface SubscriptionPollOutcome {\n subscription: UploaderSubscription;\n fetched: boolean;\n notModified: boolean;\n parsed: number;\n matched: number;\n recorded: number;\n /** Of those sent to record, how many the host deduped onto an existing row. */\n deduped: number;\n upstreamStatus: number;\n /** New ETag returned by upstream (only set when fetched=true). */\n etag: string | null;\n error: string;\n}\n\n/**\n * Build a `ReleaseCandidate` from a parsed RSS item + the matcher's verdict.\n *\n * Language is hardcoded to `\"en\"` \u2014 Nyaa releases don't carry a language tag\n * in the title or RSS metadata. English-only is the right default for the\n * uploader allowlist this plugin is designed around (`1r0n`, etc.); admins\n * who add non-English uploaders should configure tracked series' languages\n * accordingly. The host's `latest_known_*` advance gate enforces the\n * per-series language list.\n */\nfunction toCandidate(\n match: AliasMatch,\n item: ParsedRssItem,\n subscription: UploaderSubscription,\n): ReleaseCandidate {\n // Format hints carry just the recognized parser tags (`digital`, `jxl`, \u2026)\n // plus the originating subscription. Volume / chapter coverage now travels\n // as first-class span lists on the candidate itself rather than smuggled\n // through `format_hints.*RangeEnd` keys.\n const formatHints: Record<string, unknown> = { ...item.formatHints };\n formatHints.subscription = `${subscription.kind}:${subscription.identifier}`;\n\n // Nyaa RSS carries two URLs per item:\n // <guid>: the human-readable post page (`/view/<id>`)\n // <link>: the actual `.torrent` download\n // We surface the page as `payloadUrl` (the inbox's external-link icon)\n // and the torrent as `mediaUrl` with kind=torrent so the UI can render a\n // second, kind-specific icon for one-click acquisition. When the page URL\n // is missing we fall back to the torrent for `payloadUrl` and skip the\n // separate media link to avoid pointing both icons at the same URL.\n const torrentLink = item.link.length > 0 ? item.link : null;\n const payloadUrl = item.pageUrl ?? torrentLink ?? `urn:nyaa:${item.externalReleaseId}`;\n const hasDistinctMedia = item.pageUrl !== null && torrentLink !== null;\n\n return {\n seriesMatch: {\n codexSeriesId: match.seriesId,\n confidence: match.confidence,\n reason: match.reason,\n },\n externalReleaseId: item.externalReleaseId,\n volumes: item.volumes,\n chapters: item.chapters,\n language: \"en\",\n groupOrUploader: item.group ?? (subscription.kind === \"user\" ? subscription.identifier : null),\n payloadUrl,\n ...(hasDistinctMedia ? { mediaUrl: torrentLink, mediaUrlKind: \"torrent\" as const } : {}),\n infoHash: item.infoHash,\n formatHints,\n observedAt: item.observedAt,\n };\n}\n\n/**\n * Poll a single uploader subscription. Internal \u2014 exposed for testing.\n */\nexport async function pollSubscription(\n rpc: HostRpcClient,\n sourceId: string,\n subscription: UploaderSubscription,\n candidates: AliasCandidate[],\n options: {\n previousEtag: string | null;\n timeoutMs: number;\n minConfidence: number;\n baseUrl?: string | null;\n fetchImpl?: typeof fetch;\n },\n): Promise<SubscriptionPollOutcome> {\n const result = await fetchSubscriptionFeed(subscription, options.previousEtag, null, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n ...(options.baseUrl ? { baseUrl: options.baseUrl } : {}),\n });\n\n if (result.kind === \"notModified\") {\n return {\n subscription,\n fetched: true,\n notModified: true,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: 304,\n etag: null,\n error: \"\",\n };\n }\n\n if (result.kind === \"error\") {\n return {\n subscription,\n fetched: false,\n notModified: false,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: result.status,\n etag: null,\n error: result.message,\n };\n }\n\n // result.kind === \"ok\"\n const items = parseFeed(result.body);\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n for (const item of items) {\n // Prefer the alias-list form: a `Title A / Title B` Nyaa title surfaces\n // both halves in `seriesGuessAliases`, so the matcher can hit on either\n // the JP or EN side of the alias separator. Falls back to the single\n // guess for titles without a slash.\n const guesses =\n item.seriesGuessAliases.length > 0 ? item.seriesGuessAliases : [item.seriesGuess];\n const m = matchSeriesAny(guesses, candidates, {\n fuzzyFloor: options.minConfidence,\n });\n if (m === null) continue;\n matched++;\n const candidate = toCandidate(m, item, subscription);\n const outcome = await recordCandidate(rpc, sourceId, candidate);\n if (!outcome) continue;\n if (outcome.deduped) {\n deduped++;\n } else {\n recorded++;\n }\n }\n return {\n subscription,\n fetched: true,\n notModified: false,\n parsed: items.length,\n matched,\n recorded,\n deduped,\n upstreamStatus: 200,\n etag: result.etag,\n error: \"\",\n };\n}\n\n// =============================================================================\n// Top-level poll handler\n// =============================================================================\n\n/**\n * Resolve the subscription this poll request is for. The host stamps every\n * `release_sources` row with its plugin-defined `config` (set at register\n * time), so the preferred path is `params.config.subscription`. If a row\n * pre-dates the config field (e.g. created in a previous plugin version),\n * fall back to parsing `params.sourceKey`.\n */\nfunction resolveSubscription(params: ReleasePollRequest): UploaderSubscription | null {\n const cfg = params.config as { subscription?: unknown } | undefined | null;\n const fromConfig = cfg?.subscription;\n if (fromConfig && typeof fromConfig === \"object\") {\n const obj = fromConfig as Record<string, unknown>;\n const kind = obj.kind;\n const identifier = obj.identifier;\n if (\n typeof identifier === \"string\" &&\n identifier.length > 0 &&\n (kind === \"user\" || kind === \"query\" || kind === \"params\")\n ) {\n return { kind, identifier };\n }\n }\n if (typeof params.sourceKey === \"string\" && params.sourceKey.length > 0) {\n return sourceKeyToSubscription(params.sourceKey);\n }\n return null;\n}\n\nasync function poll(params: ReleasePollRequest, rpc: HostRpcClient): Promise<ReleasePollResponse> {\n const sourceId = params.sourceId;\n const subscription = resolveSubscription(params);\n if (subscription === null) {\n logger.warn(`source=${sourceId} no resolvable subscription on poll request; skipping`);\n return { notModified: false, upstreamStatus: 200 };\n }\n\n // 1. Pull tracked-series + aliases.\n const tracked = await fetchAllTracked(rpc, sourceId);\n if (tracked.length === 0) {\n logger.info(`no tracked series with aliases for source=${sourceId}`);\n return { notModified: false, upstreamStatus: 200 };\n }\n\n // 2. Conditional GET against this subscription's feed.\n const outcome = await pollSubscription(rpc, sourceId, subscription, tracked, {\n previousEtag: params.etag ?? null,\n timeoutMs: state.requestTimeoutMs,\n minConfidence: state.minConfidence,\n ...(state.baseUrl ? { baseUrl: state.baseUrl } : {}),\n });\n if (outcome.error) {\n logger.warn(\n `source=${sourceId} ${subscription.kind}:${subscription.identifier}: ${outcome.error} (status ${outcome.upstreamStatus})`,\n );\n }\n\n logger.info(\n `poll complete: source=${sourceId} subscription=${subscription.kind}:${subscription.identifier} tracked=${tracked.length} parsed=${outcome.parsed} matched=${outcome.matched} recorded=${outcome.recorded} deduped=${outcome.deduped} status=${outcome.upstreamStatus}${outcome.notModified ? \" (304)\" : \"\"}`,\n );\n\n // Report counters back to the host so it can build a meaningful\n // `last_summary` for the source. Without these, the host only sees the\n // (empty) `candidates` payload \u2014 we record via reverse-RPC mid-poll \u2014\n // and the status badge reads \"Fetched 0 items\" even on a busy poll.\n return {\n notModified: outcome.notModified,\n upstreamStatus: outcome.upstreamStatus,\n parsed: outcome.parsed,\n matched: outcome.matched,\n recorded: outcome.recorded,\n deduped: outcome.deduped,\n ...(outcome.etag !== null ? { etag: outcome.etag } : {}),\n };\n}\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\n/**\n * Send the desired-state list of source rows to the host. Called from\n * `onInitialize` (after the host has installed the releases reverse-RPC\n * handler) so the plugin's source rows are materialized whenever the\n * config changes.\n *\n * Retries on `METHOD_NOT_FOUND` with linear backoff: the host installs the\n * releases handler shortly after `initialize` returns, and there is a small\n * race window where the plugin's first reverse-RPC call may land before the\n * handler is in place.\n */\nexport async function registerSources(\n rpc: HostRpcClient,\n subscriptions: UploaderSubscription[],\n): Promise<{ registered: number; pruned: number } | null> {\n const sources = subscriptions.map((sub) => ({\n sourceKey: subscriptionToSourceKey(sub),\n displayName: displayNameFor(sub),\n kind: \"rss-uploader\" as const,\n config: { subscription: { kind: sub.kind, identifier: sub.identifier } },\n }));\n\n const maxAttempts = 5;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await rpc.call<{ registered: number; pruned: number }>(\n RELEASES_METHODS.REGISTER_SOURCES,\n { sources },\n );\n } catch (err) {\n const isMethodNotFound = err instanceof HostRpcError && err.code === -32601;\n if (isMethodNotFound && attempt < maxAttempts) {\n // Wait for the host to finish installing the releases reverse-RPC\n // handler. Linear backoff: 50ms, 100ms, 150ms, 200ms.\n await new Promise((r) => setTimeout(r, 50 * attempt));\n continue;\n }\n const reason = err instanceof Error ? err.message : String(err);\n logger.error(`register_sources failed: ${reason}`);\n return null;\n }\n }\n return null;\n}\n\n/** Human-readable label shown in the Release tracking settings table. */\nfunction displayNameFor(sub: UploaderSubscription): string {\n if (sub.kind === \"user\") return `Nyaa: ${sub.identifier}`;\n if (sub.kind === \"query\") return `Nyaa search: ${sub.identifier}`;\n return `Nyaa params: ${sub.identifier}`;\n}\n\ncreateReleaseSourcePlugin({\n manifest,\n provider: {\n async poll(params: ReleasePollRequest): Promise<ReleasePollResponse> {\n if (!state.hostRpc) {\n throw new Error(\"Plugin not initialized: hostRpc client missing\");\n }\n return poll(params, state.hostRpc);\n },\n },\n logLevel: \"info\",\n async onInitialize(params: InitializeParams) {\n state.hostRpc = params.hostRpc;\n const ac = params.adminConfig ?? {};\n state.subscriptions = parseSubscriptionList(ac.uploaders);\n if (typeof ac.requestTimeoutMs === \"number\" && Number.isFinite(ac.requestTimeoutMs)) {\n state.requestTimeoutMs = Math.max(1_000, Math.min(ac.requestTimeoutMs, 60_000));\n }\n if (typeof ac.baseUrl === \"string\" && ac.baseUrl.trim().length > 0) {\n state.baseUrl = ac.baseUrl.trim();\n }\n logger.info(\n `initialized: subscriptions=${state.subscriptions.length} timeoutMs=${state.requestTimeoutMs} minConfidence=${state.minConfidence}`,\n );\n\n // Materialize source rows. Deferred to a microtask + retry on\n // METHOD_NOT_FOUND so we run *after* the host installs the releases\n // reverse-RPC handler (it does so right after `initialize` returns).\n queueMicrotask(() => {\n void registerSources(params.hostRpc, state.subscriptions).then((result) => {\n if (result) {\n logger.info(`register_sources: registered=${result.registered} pruned=${result.pruned}`);\n }\n });\n });\n },\n});\n\nlogger.info(\"Nyaa release-source plugin started\");\n"],
5
+ "mappings": "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",
6
6
  "names": ["manifest", "logger", "response", "manifest"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ashdev/codex-plugin-release-nyaa",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "Nyaa.si uploader-feed release-source plugin for Codex - announces torrent releases for tracked series, filtered by an admin allowlist of trusted uploaders",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -40,7 +40,7 @@
40
40
  "node": ">=22.0.0"
41
41
  },
42
42
  "dependencies": {
43
- "@ashdev/codex-plugin-sdk": "^1.20.0"
43
+ "@ashdev/codex-plugin-sdk": "^1.21.1"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "^2.4.4",