@ashdev/codex-plugin-release-mangaupdates 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). */
@@ -656,13 +675,13 @@ function splitItems(xml) {
656
675
  function parseTitle(title) {
657
676
  const trimmed = title.trim();
658
677
  let chapter = null;
659
- const chMatch = trimmed.match(/\bc(?:h)?\.?\s*([0-9]+(?:\.[0-9]+)?)\b/i);
678
+ const chMatch = trimmed.match(/\bc(?:h)?\.?\s*([0-9]+(?:\.[0-9]+)?)[a-z]?(?![0-9])/i);
660
679
  if (chMatch?.[1]) {
661
680
  const n = Number.parseFloat(chMatch[1]);
662
681
  if (Number.isFinite(n)) chapter = n;
663
682
  }
664
683
  let volume = null;
665
- const volMatch = trimmed.match(/\bv(?:ol)?\.?\s*([0-9]+)\b/i);
684
+ const volMatch = trimmed.match(/\bv(?:ol)?\.?\s*([0-9]+)[a-z]?(?![0-9])/i);
666
685
  if (volMatch?.[1]) {
667
686
  const n = Number.parseInt(volMatch[1], 10);
668
687
  if (Number.isFinite(n)) volume = n;
@@ -687,10 +706,10 @@ function pubDateToIso(raw) {
687
706
  }
688
707
  return (/* @__PURE__ */ new Date()).toISOString();
689
708
  }
690
- function deriveExternalReleaseId(guid, link, title, pubDate) {
709
+ function deriveExternalReleaseId(guid, link, title, group, pubDate) {
691
710
  if (guid && guid.trim().length > 0) return guid.trim();
692
711
  if (link && link.trim().length > 0) return link.trim();
693
- const fallback = `${title}|${pubDate ?? ""}`;
712
+ const fallback = `${title}|${group ?? ""}|${pubDate ?? ""}`;
694
713
  let h = 5381;
695
714
  for (let i = 0; i < fallback.length; i++) {
696
715
  h = (h << 5) + h + fallback.charCodeAt(i) | 0;
@@ -703,9 +722,13 @@ function parseItem(itemXml) {
703
722
  const link = extractTagText(itemXml, "link");
704
723
  const guid = extractTagText(itemXml, "guid");
705
724
  const pubDate = extractTagText(itemXml, "pubDate");
706
- const { chapter, volume, group, language } = parseTitle(title);
725
+ const description = extractTagText(itemXml, "description");
726
+ const { chapter, volume, group: groupFromTitle, language } = parseTitle(title);
727
+ if (chapter === null && volume === null) return null;
728
+ const descTrimmed = description?.trim();
729
+ const group = descTrimmed && descTrimmed.length > 0 ? descTrimmed : groupFromTitle;
707
730
  return {
708
- externalReleaseId: deriveExternalReleaseId(guid, link, title, pubDate),
731
+ externalReleaseId: deriveExternalReleaseId(guid, link, title, group, pubDate),
709
732
  title,
710
733
  chapter,
711
734
  volume,
@@ -716,7 +739,17 @@ function parseItem(itemXml) {
716
739
  };
717
740
  }
718
741
  function parseFeed(xml) {
719
- return splitItems(xml).map(parseItem).filter((i) => i !== null);
742
+ return {
743
+ channelLink: extractChannelLink(xml),
744
+ items: splitItems(xml).map(parseItem).filter((i) => i !== null)
745
+ };
746
+ }
747
+ function extractChannelLink(xml) {
748
+ const stripped = xml.replace(/<item\b[^>]*>[\s\S]*?<\/item>/gi, "");
749
+ const link = extractTagText(stripped, "link");
750
+ if (!link) return null;
751
+ const trimmed = link.trim();
752
+ return trimmed.length > 0 ? trimmed : null;
720
753
  }
721
754
 
722
755
  // src/filter.ts
@@ -763,7 +796,7 @@ function dedupePreserveOrder(xs) {
763
796
  // package.json
764
797
  var package_default = {
765
798
  name: "@ashdev/codex-plugin-release-mangaupdates",
766
- version: "1.20.0",
799
+ version: "1.21.1",
767
800
  description: "MangaUpdates RSS release-source plugin for Codex - announces new chapter releases for tracked series in user-configured languages",
768
801
  main: "dist/index.js",
769
802
  bin: "dist/index.js",
@@ -802,7 +835,7 @@ var package_default = {
802
835
  node: ">=22.0.0"
803
836
  },
804
837
  dependencies: {
805
- "@ashdev/codex-plugin-sdk": "^1.20.0"
838
+ "@ashdev/codex-plugin-sdk": "^1.21.1"
806
839
  },
807
840
  devDependencies: {
808
841
  "@biomejs/biome": "^2.4.4",
@@ -877,6 +910,35 @@ async function listTracked(rpc, sourceId, offset, limit) {
877
910
  limit
878
911
  });
879
912
  }
913
+ async function countTracked(rpc, sourceId) {
914
+ try {
915
+ const r = await rpc.call(RELEASES_METHODS.COUNT_TRACKED, {
916
+ sourceId
917
+ });
918
+ return r.total;
919
+ } catch (err) {
920
+ if (err instanceof HostRpcError && err.code === -32601) {
921
+ return null;
922
+ }
923
+ const reason = err instanceof Error ? err.message : String(err);
924
+ logger.warn(`count_tracked failed for ${sourceId}: ${reason}`);
925
+ return null;
926
+ }
927
+ }
928
+ async function reportProgress(rpc, current, total, message) {
929
+ try {
930
+ await rpc.call(
931
+ RELEASES_METHODS.REPORT_PROGRESS,
932
+ message !== void 0 ? { current, total, message } : { current, total }
933
+ );
934
+ } catch (err) {
935
+ if (err instanceof HostRpcError && err.code === -32601) {
936
+ return;
937
+ }
938
+ const reason = err instanceof Error ? err.message : String(err);
939
+ logger.debug(`report_progress dropped: ${reason}`);
940
+ }
941
+ }
880
942
  async function recordCandidate(rpc, sourceId, candidate) {
881
943
  try {
882
944
  return await rpc.call(RELEASES_METHODS.RECORD, {
@@ -910,7 +972,8 @@ async function* iterateTrackedSeries(rpc, sourceId) {
910
972
  function effectiveLanguagesForSeries(_entry) {
911
973
  return [];
912
974
  }
913
- function toCandidate(entry, item) {
975
+ function toCandidate(entry, item, channelLink) {
976
+ const payloadUrl = item.link.length > 0 ? item.link : channelLink && channelLink.length > 0 ? channelLink : `urn:mu:${item.externalReleaseId}`;
914
977
  const candidate = {
915
978
  seriesMatch: {
916
979
  codexSeriesId: entry.seriesId,
@@ -925,7 +988,7 @@ function toCandidate(entry, item) {
925
988
  chapters: item.chapter === null ? null : [{ start: item.chapter, end: item.chapter }],
926
989
  language: item.language,
927
990
  groupOrUploader: item.group,
928
- payloadUrl: item.link.length > 0 ? item.link : `urn:mu:${item.externalReleaseId}`,
991
+ payloadUrl,
929
992
  observedAt: item.observedAt
930
993
  };
931
994
  return candidate;
@@ -978,7 +1041,7 @@ async function pollSeries(rpc, sourceId, entry, options) {
978
1041
  error: result.message
979
1042
  };
980
1043
  }
981
- const items = parseFeed(result.body);
1044
+ const { items, channelLink } = parseFeed(result.body);
982
1045
  const filters = resolveFilters({
983
1046
  languages: effectiveLanguagesForSeries(entry),
984
1047
  blockedGroups: options.blockedGroups
@@ -989,7 +1052,7 @@ async function pollSeries(rpc, sourceId, entry, options) {
989
1052
  for (const item of items) {
990
1053
  if (!passesFilters(item, filters)) continue;
991
1054
  matched++;
992
- const candidate = toCandidate(entry, item);
1055
+ const candidate = toCandidate(entry, item, channelLink);
993
1056
  const outcome = await recordCandidate(rpc, sourceId, candidate);
994
1057
  if (!outcome) continue;
995
1058
  if (outcome.deduped) {
@@ -1014,6 +1077,7 @@ async function pollSeries(rpc, sourceId, entry, options) {
1014
1077
  async function poll(params, rpc) {
1015
1078
  const sourceId = params.sourceId;
1016
1079
  const blockedGroups = parseCommaList(state.blockedGroupsCsv);
1080
+ const total = await countTracked(rpc, sourceId);
1017
1081
  let parsed = 0;
1018
1082
  let matched = 0;
1019
1083
  let recorded = 0;
@@ -1041,6 +1105,12 @@ async function poll(params, rpc) {
1041
1105
  } else if (outcome.error) {
1042
1106
  logger.warn(`series ${entry.seriesId}: ${outcome.error} (status ${outcome.upstreamStatus})`);
1043
1107
  }
1108
+ await reportProgress(
1109
+ rpc,
1110
+ seenSeries,
1111
+ total ?? seenSeries,
1112
+ `Polled ${seenSeries}${total !== null ? `/${total}` : ""} series`
1113
+ );
1044
1114
  }
1045
1115
  if (skippedNoMuId > 0) {
1046
1116
  logger.info(
@@ -1124,6 +1194,7 @@ createReleaseSourcePlugin({
1124
1194
  logger.info("MangaUpdates release-source plugin started");
1125
1195
  export {
1126
1196
  _resetState,
1197
+ poll,
1127
1198
  pollSeries,
1128
1199
  registerSources
1129
1200
  };
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", "../src/parser.ts", "../src/filter.ts", "../package.json", "../src/manifest.ts", "../src/index.ts"],
4
- "sourcesContent": [null, null, null, null, null, null, null, null, "/**\n * MangaUpdates per-series RSS fetcher.\n *\n * Wraps `fetch` with conditional GET (`If-None-Match` from a stored ETag) and\n * a hard timeout. 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; 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}\n\n/** Public base URL for MangaUpdates' v1 RSS API. */\nexport const MANGAUPDATES_RSS_BASE = \"https://api.mangaupdates.com/v1/series\";\n\n/**\n * Normalize a MangaUpdates series ID to its numeric form for API calls.\n *\n * MangaUpdates uses two interchangeable representations of the same ID:\n *\n * - **Numeric** (e.g. `15180124327`) \u2014 the internal primary key. Every\n * `/v1/series/...` API endpoint requires this form.\n * - **Base36 slug** (e.g. `6z1uqw7`) \u2014 a base36 encoding of the numeric\n * ID, used in public URLs only (`mangaupdates.com/series/6z1uqw7/...`).\n * The API rejects this form with a 405.\n *\n * Metadata sources (MangaBaka, etc.) typically scrape the public URL and\n * store the slug, so the value we receive on `entry.externalIds.mangaupdates`\n * is whatever the source happened to grab. Decode here so callers don't\n * have to know.\n *\n * Returns the input unchanged when it's already an all-digit string;\n * `null` when the input contains characters outside the base36 alphabet\n * (caller should surface as a configuration error).\n */\nexport function normalizeMangaUpdatesId(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n if (/^\\d+$/.test(trimmed)) return trimmed;\n if (!/^[0-9a-z]+$/i.test(trimmed)) return null;\n // parseInt('6z1uqw7', 36) = 15180124327. JS numbers are precise for\n // integers up to 2^53; MangaUpdates IDs sit well below that.\n const decoded = Number.parseInt(trimmed, 36);\n if (!Number.isFinite(decoded) || decoded <= 0) return null;\n return String(decoded);\n}\n\n/**\n * Build the per-series RSS URL. Accepts either the numeric ID or the\n * base36 slug \u2014 see `normalizeMangaUpdatesId` for the rationale.\n */\nexport function feedUrl(mangaUpdatesId: string): string {\n const normalized = normalizeMangaUpdatesId(mangaUpdatesId) ?? mangaUpdatesId;\n return `${MANGAUPDATES_RSS_BASE}/${normalized}/rss`;\n}\n\n/**\n * Conditional GET against a per-series RSS feed.\n *\n * @param mangaUpdatesId - The MangaUpdates series ID.\n * @param previousEtag - The ETag from the previous successful poll (if any).\n * @param opts - Fetcher options (custom fetch, timeout).\n */\nexport async function fetchSeriesFeed(\n mangaUpdatesId: string,\n previousEtag: string | null,\n opts: FetcherOptions = {},\n): Promise<FetchResult> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n const timeoutMs = opts.timeoutMs ?? 10_000;\n\n const url = feedUrl(mangaUpdatesId);\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\n // AbortSignal.timeout is the cleanest path. Falling back to a manual\n // controller would add complexity without value (we already require Node\n // 22+).\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 // Treat aborts and other transport-level failures as 0/unavailable so\n // the host's per-host backoff layer can detect \"this domain is sad\n // right now\" without us having to invent a fake HTTP status.\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 return { kind: \"ok\", body, etag, status: 200 };\n }\n\n // Pass through 429 / 5xx so the host's backoff layer sees the real status.\n return {\n kind: \"error\",\n status: resp.status,\n message: `upstream returned ${resp.status} ${resp.statusText}`,\n };\n}\n", "/**\n * RSS parser for MangaUpdates per-series feeds.\n *\n * Per-series feed: `https://api.mangaupdates.com/v1/series/{series_id}/rss`\n *\n * Each `<item>` is one scanlation release. The plugin extracts:\n * - chapter / volume from the title\n * - scanlation group from the title\n * - language tag (parenthesized two-letter code) from the title\n * - link (the MangaUpdates release page) used as `payloadUrl`\n * - pubDate as `observedAt`\n *\n * Implementation note: we do NOT pull in a heavy XML parser. The MangaUpdates\n * RSS format is simple, well-formed, and stable. A small targeted regex\n * pipeline avoids a 100kb dependency and CVE surface for marginal benefit.\n */\n\n/** Parsed item, pre-`ReleaseCandidate`. */\nexport interface ParsedRssItem {\n /** Stable per-source ID. Derived from the release URL or guid. */\n externalReleaseId: string;\n /** Original title string. Useful for debugging / fallback. */\n title: string;\n /** Chapter number (decimals supported, e.g. \"47.5\"). */\n chapter: number | null;\n /** Volume number. */\n volume: number | null;\n /**\n * Language tag (lowercased ISO 639-1). Defaults to `\"en\"` when the title\n * doesn't carry an explicit `(xx)` code, since the MangaUpdates v1 RSS\n * endpoint serves the English release stream. The legacy\n * `UNKNOWN_LANGUAGE` sentinel is still exported for callers that want\n * to surface \"no tag detected\" explicitly, but the parser no longer\n * produces it on its own.\n */\n language: string;\n /** Scanlation group name (best-effort; nullable). */\n group: string | null;\n /** Release page URL on MangaUpdates. Used as `payloadUrl`. */\n link: string;\n /** ISO-8601 string. Falls back to \"now\" when pubDate is missing/invalid. */\n observedAt: string;\n}\n\n/** Sentinel returned when the language tag can't be detected. */\nexport const UNKNOWN_LANGUAGE = \"unknown\" as const;\n\n// -----------------------------------------------------------------------------\n// XML helpers\n// -----------------------------------------------------------------------------\n\n/** Strip CDATA wrapper if present, unescape `&amp;` `&lt;` `&gt;` `&quot;`. */\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 const re = new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)</${tag}>`, \"i\");\n const m = xml.match(re);\n if (!m?.[1]) return null;\n return decodeXmlText(m[1]);\n}\n\n/** Pull all `<item>...</item>` blocks from a feed. */\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 * Extract chapter/volume/group/language from a MangaUpdates RSS title.\n *\n * Observed shapes:\n * \"Vol.2 c.14 by GroupName (en)\"\n * \"v.2 c.14.5 by GroupName (es)\"\n * \"c.143 by GroupName\" (language missing)\n * \"Vol.15 by GroupName (en)\" (volume-only bundle)\n * \"c.143 (en)\" (no group)\n *\n * Volume tokens: `v.N`, `vol.N`, `Vol.N` (case-insensitive).\n * Chapter tokens: `c.N`, `ch.N`, `Ch.N` (decimals allowed).\n * Group: text between `by ` and the next `(` or end-of-string.\n * Language: trailing `(xx)` two-letter code, lowercased.\n */\nexport function parseTitle(title: string): {\n chapter: number | null;\n volume: number | null;\n group: string | null;\n language: string;\n} {\n const trimmed = title.trim();\n\n // Chapter: c.N or ch.N (allow decimals).\n let chapter: number | null = null;\n const chMatch = trimmed.match(/\\bc(?:h)?\\.?\\s*([0-9]+(?:\\.[0-9]+)?)\\b/i);\n if (chMatch?.[1]) {\n const n = Number.parseFloat(chMatch[1]);\n if (Number.isFinite(n)) chapter = n;\n }\n\n // Volume: v.N or vol.N.\n let volume: number | null = null;\n const volMatch = trimmed.match(/\\bv(?:ol)?\\.?\\s*([0-9]+)\\b/i);\n if (volMatch?.[1]) {\n const n = Number.parseInt(volMatch[1], 10);\n if (Number.isFinite(n)) volume = n;\n }\n\n // Group: \"by <Group>\" up to \"(\" or end.\n let group: string | null = null;\n const groupMatch = trimmed.match(/\\bby\\s+(.+?)(?:\\s*\\([a-z]{2,3}\\)\\s*)?$/i);\n if (groupMatch?.[1]) {\n const candidate = groupMatch[1].trim();\n if (candidate.length > 0) group = candidate;\n }\n\n // Language: trailing parenthesized 2-3 letter code (e.g. (en), (es), (id), (por)).\n //\n // The current MangaUpdates v1 RSS endpoint (`/v1/series/{id}/rss`) ships\n // titles without a language tag \u2014 it's the English-localized release\n // stream by design. Default to `\"en\"` so items aren't dropped by the\n // client-side language gate; an explicit `(es)` / `(id)` / etc. still\n // wins when present, and the host's per-series language list remains\n // the authoritative gate downstream. The legacy `UNKNOWN_LANGUAGE`\n // sentinel is kept exported for backwards compatibility but no longer\n // produced by this parser.\n let language = \"en\";\n const langMatch = trimmed.match(/\\(([a-z]{2,3})\\)\\s*$/i);\n if (langMatch?.[1]) {\n language = langMatch[1].toLowerCase();\n }\n\n return { chapter, volume, group, language };\n}\n\n// -----------------------------------------------------------------------------\n// Item parsing\n// -----------------------------------------------------------------------------\n\n/**\n * Best-effort `pubDate` -> ISO-8601 conversion. MangaUpdates uses RFC-2822\n * style dates (`Mon, 04 May 2026 02:31:00 GMT`). Falls back to \"now\" on\n * invalid input \u2014 never throws, since one bad pubDate shouldn't drop the\n * whole feed.\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 * Derive a stable external_release_id. Prefer `<guid>`, then the link URL,\n * otherwise fall back to a deterministic hash of `(title + pubDate)`.\n *\n * Stability is what matters: re-polling the same item must produce the same\n * ID so the host's `(source_id, external_release_id)` dedup catches it.\n */\nfunction deriveExternalReleaseId(\n guid: string | null,\n link: 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 // Deterministic fallback for feeds that omit both. djb2-ish hash keeps the\n // ID short while staying stable across polls.\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 MangaUpdates `<item>` block into a `ParsedRssItem`. Returns\n * null if the title is missing entirely (truly malformed item).\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\n const { chapter, volume, group, language } = parseTitle(title);\n\n return {\n externalReleaseId: deriveExternalReleaseId(guid, link, title, pubDate),\n title,\n chapter,\n volume,\n group,\n language,\n link: link ?? \"\",\n observedAt: pubDateToIso(pubDate),\n };\n}\n\n/**\n * Parse a full MangaUpdates per-series RSS feed body into items. Bad items\n * (missing title) are dropped silently \u2014 the feed should be best-effort\n * tolerant.\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 * Filtering: language allowlist + group blocklist.\n *\n * Filters are applied client-side in the plugin (before recording) for two\n * reasons:\n * 1. Keeps the ledger small. Out-of-language items would be dropped by the\n * host anyway via the latest_known_* gate, but writing them to the\n * ledger pollutes the inbox and wastes write IO.\n * 2. Keeps the inbox clean. Users who configure `[\"en\"]` don't want to see\n * Spanish entries hidden behind a state flag \u2014 they want them gone.\n */\n\nimport { type ParsedRssItem, UNKNOWN_LANGUAGE } from \"./parser.js\";\n\n/**\n * Resolved, normalized filter inputs for a single series. Both lists are\n * lowercased + trimmed. Empty `languages` is interpreted as \"no filter\"\n * (everything passes), but the caller is expected to pass at least the\n * server-wide default to avoid that footgun.\n */\nexport interface ResolvedFilters {\n /** Lowercased ISO 639-1 codes; empty = no filter. */\n languages: string[];\n /** Lowercased group names; case-insensitive exact match against `group`. */\n blockedGroups: Set<string>;\n /**\n * Whether to include items whose language couldn't be detected\n * (`UNKNOWN_LANGUAGE` sentinel). Default false \u2014 be conservative.\n */\n includeUnknownLanguage: boolean;\n}\n\n/**\n * Build resolved filters from raw config strings + lists. Centralizes the\n * normalization so the poll handler doesn't have to care about casing or\n * whitespace.\n */\nexport function resolveFilters(input: {\n languages: string[];\n blockedGroups: string[];\n includeUnknownLanguage?: boolean;\n}): ResolvedFilters {\n const languages = dedupePreserveOrder(\n input.languages.map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0),\n );\n const blockedGroups = new Set(\n input.blockedGroups.map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0),\n );\n return {\n languages,\n blockedGroups,\n includeUnknownLanguage: input.includeUnknownLanguage ?? false,\n };\n}\n\n/**\n * Parse a comma-separated string into a clean list (trim, drop empties).\n * Helper for `blockedGroups` which is admin-config typed as a single string.\n */\nexport function parseCommaList(raw: unknown): string[] {\n if (typeof raw !== \"string\") return [];\n return raw\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Returns true if the item should be kept.\n *\n * Language filter:\n * - If `languages` is empty \u2192 pass.\n * - Otherwise, item.language must be in the list (case-insensitive).\n * - `unknown` language is rejected unless `includeUnknownLanguage` is true.\n *\n * Group filter:\n * - If `group` is null \u2192 pass (we have nothing to match against).\n * - Otherwise, group must NOT be in `blockedGroups`.\n */\nexport function passesFilters(item: ParsedRssItem, filters: ResolvedFilters): boolean {\n // Language gate.\n if (item.language === UNKNOWN_LANGUAGE) {\n if (!filters.includeUnknownLanguage) return false;\n } else if (filters.languages.length > 0) {\n if (!filters.languages.includes(item.language.toLowerCase())) return false;\n }\n\n // Group blocklist.\n if (item.group !== null && filters.blockedGroups.size > 0) {\n if (filters.blockedGroups.has(item.group.trim().toLowerCase())) return false;\n }\n\n return true;\n}\n\nfunction dedupePreserveOrder(xs: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const x of xs) {\n if (!seen.has(x)) {\n seen.add(x);\n out.push(x);\n }\n }\n return out;\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-release-mangaupdates\",\n \"version\": \"1.20.0\",\n \"description\": \"MangaUpdates RSS release-source plugin for Codex - announces new chapter releases for tracked series in user-configured languages\",\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-mangaupdates\"\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 \"mangaupdates\",\n \"release-source\",\n \"manga\"\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/**\n * External-ID source name for MangaUpdates.\n *\n * MangaUpdates IDs are populated by metadata-provider plugins (e.g.\n * MangaBaka cross-references) or pasted manually by the user via the series\n * tracking panel. The release plugin needs the bare source name (no\n * `plugin:` prefix) here to match the host's external-ID filter.\n */\nexport const EXTERNAL_ID_SOURCE_MANGAUPDATES = \"mangaupdates\" as const;\n\nexport const manifest = {\n name: \"release-mangaupdates\",\n displayName: \"MangaUpdates Releases\",\n version: packageJson.version,\n description:\n \"Announces new chapter releases for tracked series via MangaUpdates per-series RSS feeds. Filters by user-configured languages.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n releaseSource: {\n kinds: [\"rss-series\"],\n requiresAliases: false,\n requiresExternalIds: [EXTERNAL_ID_SOURCE_MANGAUPDATES],\n canAnnounceChapters: true,\n canAnnounceVolumes: true,\n },\n },\n configSchema: {\n description:\n \"MangaUpdates plugin configuration. Per-series language preferences live on each series' tracking config; the values here are server-wide defaults applied when a series doesn't override them.\",\n fields: [\n {\n key: \"blockedGroups\",\n label: \"Blocked Scanlation Groups\",\n description:\n \"Comma-separated list of scanlation group names to exclude from announcements (case-insensitive, exact match). Per-series overrides may further extend this list.\",\n type: \"string\" as const,\n required: false,\n default: \"\",\n example: \"LowQualityScans,MTL Group\",\n },\n {\n key: \"requestTimeoutMs\",\n label: \"Request Timeout (ms)\",\n description:\n \"How long to wait for a single RSS fetch before giving up. Defaults to 10000 (10 seconds).\",\n type: \"number\" as const,\n required: false,\n default: 10_000,\n },\n ],\n },\n userDescription:\n \"Announces new chapters for series you've tracked, using their MangaUpdates IDs. Filters releases to languages you can read. Notification-only \u2014 Codex does not download anything.\",\n adminSetupInstructions:\n \"1. No config is required to get started \u2014 saving the plugin is enough. The plugin auto-registers a single source row (`MangaUpdates Releases`) in **Settings \u2192 Release tracking** on first start, where you can disable it, change the poll interval, or hit *Poll now*. 2. To get announcements for a series, edit its tracking panel and either paste a `mangaupdates` external ID or let the metadata-refresh path populate it from MangaBaka cross-references. 3. Optional: set `blockedGroups` (CSV, case-insensitive) to filter noisy scanlators server-wide; per-series language preferences live on each series' tracking config and override the server default (`release_tracking.default_languages`). No credentials are needed; MangaUpdates RSS feeds are public.\",\n} as const satisfies PluginManifest & {\n capabilities: { releaseSource: { kinds: [\"rss-series\"] } };\n};\n", "/**\n * MangaUpdates RSS Release-Source Plugin for Codex.\n *\n * Polls per-series RSS feeds at MangaUpdates and announces new chapter /\n * volume releases for tracked series. The plugin is the first writer of\n * `release_ledger` rows in production \u2014 earlier phases build the\n * infrastructure, this one delivers the first real notification feed.\n *\n * Flow per `releases/poll`:\n * 1. Pull tracked-series scope from the host (`releases/list_tracked`).\n * Filtered server-side to series with a `mangaupdates` external ID.\n * 2. For each series, conditional GET the RSS feed.\n * 3. Parse the response into items, then filter by:\n * - per-series language list (admin / per-series config)\n * - admin-configured group blocklist\n * 4. Build `ReleaseCandidate` rows and stream them via\n * `releases/record`. The host's matcher applies the threshold and\n * ledger dedup.\n * 5. Pass the new ETag back via the poll response so the host updates\n * the source row.\n *\n * **Concurrency note:** The plugin host already serializes RPCs per plugin\n * process, so we don't need to throttle internally beyond an in-poll loop\n * that walks tracked series sequentially.\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 { fetchSeriesFeed } from \"./fetcher.js\";\nimport { parseCommaList, passesFilters, resolveFilters } from \"./filter.js\";\nimport { EXTERNAL_ID_SOURCE_MANGAUPDATES, manifest } from \"./manifest.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 /** Admin-configured group blocklist (lowercased exact match). */\n blockedGroupsCsv: string;\n /** Hard timeout for upstream fetches. */\n requestTimeoutMs: number;\n}\n\nconst state: PluginState = {\n hostRpc: null,\n blockedGroupsCsv: \"\",\n requestTimeoutMs: 10_000,\n};\n\n/** Reset state. Exported for tests; not part of the plugin contract. */\nexport function _resetState(): void {\n state.hostRpc = null;\n state.blockedGroupsCsv = \"\";\n state.requestTimeoutMs = 10_000;\n}\n\n// =============================================================================\n// Reverse-RPC wrappers (typed shorthands so the poll code reads cleanly)\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 // Threshold rejection / validation error / unknown source. Log and\n // skip; the next poll will retry the still-eligible candidates.\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 * Lazily walk all tracked-series pages from the host. Yields entries one\n * series at a time so the caller can interleave per-series fetches without\n * buffering the whole list (relevant for users tracking hundreds of series).\n */\nasync function* iterateTrackedSeries(\n rpc: HostRpcClient,\n sourceId: string,\n): AsyncGenerator<TrackedSeriesEntry> {\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 yield entry;\n }\n if (page.nextOffset === undefined || page.tracked.length === 0) return;\n offset = page.nextOffset;\n }\n}\n\n/**\n * Per-series effective language list. We use the host's `latestKnown*`\n * exposure plus the `externalIds` map to scope the fetch, but the\n * languages config is owned by the host (set on `series_tracking.languages`\n * with fallback to the server-wide default).\n *\n * However, the current `releases/list_tracked` response shape doesn't\n * expose per-series `languages` \u2014 see plan doc for this design choice.\n * For Phase 6 the plugin reads its admin-level group blocklist and emits\n * candidates with the language tag from the parsed entry; the host's\n * `latest_known_*` advance gate enforces the per-series language list\n * authoritatively (see `services/release/languages.rs`).\n *\n * We *also* want to drop out-of-language candidates client-side to keep the\n * ledger small and the inbox clean. Without per-series languages on the\n * tracked-series payload, the client-side filter degrades to a no-op\n * pass-everything for known languages \u2014 leaving it to the host's gate. The\n * group blocklist still applies.\n *\n * If a future protocol revision exposes `effectiveLanguages` on the\n * tracked-series entry, swap this stub for the real list and the existing\n * `passesFilters` will do the right thing.\n */\nfunction effectiveLanguagesForSeries(_entry: TrackedSeriesEntry): string[] {\n return []; // empty = no client-side language gate; host gate is authoritative\n}\n\n/**\n * Map a `ParsedRssItem` to a `ReleaseCandidate`. Confidence is 1.0 because\n * the match is keyed by external ID \u2014 there's no fuzzy matching.\n */\nfunction toCandidate(entry: TrackedSeriesEntry, item: ParsedRssItem): ReleaseCandidate {\n const candidate: ReleaseCandidate = {\n seriesMatch: {\n codexSeriesId: entry.seriesId,\n confidence: 1.0,\n reason: `mangaupdates_id:${entry.externalIds?.[EXTERNAL_ID_SOURCE_MANGAUPDATES] ?? \"\"}`,\n },\n externalReleaseId: item.externalReleaseId,\n // MangaUpdates always reports a single chapter / volume per release.\n // Wrap as one-element span lists for the new SDK shape; `null` when\n // the parser didn't see a value at all.\n volumes: item.volume === null ? null : [{ start: item.volume, end: item.volume }],\n chapters: item.chapter === null ? null : [{ start: item.chapter, end: item.chapter }],\n language: item.language,\n groupOrUploader: item.group,\n payloadUrl: item.link.length > 0 ? item.link : `urn:mu:${item.externalReleaseId}`,\n observedAt: item.observedAt,\n };\n return candidate;\n}\n\n// =============================================================================\n// Per-series poll\n// =============================================================================\n\n/** Outcome of a single per-series fetch+record cycle. */\nexport interface SeriesPollOutcome {\n seriesId: string;\n fetched: boolean;\n notModified: boolean;\n parsed: number;\n /** Of those parsed, how many passed client-side filters and were sent to record. */\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 if the per-series fetch failed; empty otherwise. */\n error: string;\n}\n\n/**\n * Poll a single series. Internal \u2014 exposed for testing.\n *\n * Aggregates the worst (highest) upstream status across the per-series\n * fetches at the call site so the host's per-host backoff layer sees real\n * 429/5xx signals.\n */\nexport async function pollSeries(\n rpc: HostRpcClient,\n sourceId: string,\n entry: TrackedSeriesEntry,\n options: {\n blockedGroups: string[];\n timeoutMs: number;\n fetchImpl?: typeof fetch;\n },\n): Promise<SeriesPollOutcome> {\n const muId = entry.externalIds?.[EXTERNAL_ID_SOURCE_MANGAUPDATES];\n if (!muId) {\n return {\n seriesId: entry.seriesId,\n fetched: false,\n notModified: false,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: 0,\n etag: null,\n error: \"missing mangaupdates external ID\",\n };\n }\n\n // We don't have per-series ETag here \u2014 that lives on the source row, not\n // the series. For a per-source feed (rss-uploader) ETags align cleanly;\n // for per-series feeds (this plugin) we'd need per-(source, series) state\n // to do conditional GETs per series. That's a future optimization; for\n // now we always do an unconditional GET. Daily polls + small per-series\n // bodies keep the bandwidth cost negligible.\n const result = await fetchSeriesFeed(muId, null, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n });\n\n if (result.kind === \"notModified\") {\n return {\n seriesId: entry.seriesId,\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 seriesId: entry.seriesId,\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 const filters = resolveFilters({\n languages: effectiveLanguagesForSeries(entry),\n blockedGroups: options.blockedGroups,\n });\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n for (const item of items) {\n if (!passesFilters(item, filters)) continue;\n matched++;\n const candidate = toCandidate(entry, item);\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 seriesId: entry.seriesId,\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\nasync function poll(params: ReleasePollRequest, rpc: HostRpcClient): Promise<ReleasePollResponse> {\n const sourceId = params.sourceId;\n const blockedGroups = parseCommaList(state.blockedGroupsCsv);\n\n let parsed = 0;\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n let worstStatus = 200;\n let lastEtag: string | null = null;\n let seenSeries = 0;\n // Series the host returned that lack a MangaUpdates external ID. A high\n // count here is the most common cause of an \"empty\" poll: the plugin\n // can't fetch a feed without an MU ID, so the user needs to populate\n // those (manual paste or metadata refresh from MangaBaka).\n let skippedNoMuId = 0;\n\n for await (const entry of iterateTrackedSeries(rpc, sourceId)) {\n seenSeries++;\n const outcome = await pollSeries(rpc, sourceId, entry, {\n blockedGroups,\n timeoutMs: state.requestTimeoutMs,\n });\n parsed += outcome.parsed;\n matched += outcome.matched;\n recorded += outcome.recorded;\n deduped += outcome.deduped;\n if (outcome.upstreamStatus > worstStatus) {\n worstStatus = outcome.upstreamStatus;\n }\n if (outcome.etag) lastEtag = outcome.etag;\n\n if (outcome.error === \"missing mangaupdates external ID\") {\n skippedNoMuId++;\n } else if (outcome.error) {\n logger.warn(`series ${entry.seriesId}: ${outcome.error} (status ${outcome.upstreamStatus})`);\n }\n }\n\n if (skippedNoMuId > 0) {\n logger.info(\n `skipped ${skippedNoMuId} of ${seenSeries} tracked series for source=${sourceId}: no mangaupdates external ID. Add one in the Tracking panel or run a metadata refresh.`,\n );\n }\n\n logger.info(\n `poll complete: source=${sourceId} series=${seenSeries} skipped=${skippedNoMuId} parsed=${parsed} matched=${matched} recorded=${recorded} deduped=${deduped} worst_status=${worstStatus}`,\n );\n\n // Report counters back to the host so the source's `last_summary` is\n // accurate. Without these the host only sees the (empty) `candidates`\n // payload \u2014 we record via reverse-RPC mid-poll \u2014 and the badge reads\n // \"Fetched 0 items\" no matter what actually happened.\n // Per-series ETags don't align with the per-source state slot, so we\n // intentionally leave `etag` undefined unless we actually saw one\n // (which today we won't, since we don't pass If-None-Match per series).\n return {\n notModified: false,\n upstreamStatus: worstStatus,\n parsed,\n matched,\n recorded,\n deduped,\n ...(lastEtag !== null ? { etag: lastEtag } : {}),\n };\n}\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\n/**\n * Register a single static source row representing the MangaUpdates batch\n * feed. Unlike Nyaa (one row per uploader), MangaUpdates polls all tracked\n * series under one logical feed, so we always declare exactly one row keyed\n * `default`. Retries on `METHOD_NOT_FOUND` to handle the brief race where\n * the host has not yet installed the releases reverse-RPC handler.\n */\nexport async function registerSources(\n rpc: HostRpcClient,\n): Promise<{ registered: number; pruned: number } | null> {\n const sources = [\n {\n sourceKey: \"default\",\n displayName: \"MangaUpdates Releases\",\n kind: \"rss-series\" as const,\n config: null,\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 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\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 if (typeof ac.blockedGroups === \"string\") {\n state.blockedGroupsCsv = ac.blockedGroups;\n }\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 logger.info(\n `initialized: blockedGroups=${state.blockedGroupsCsv ? \"set\" : \"empty\"} timeoutMs=${state.requestTimeoutMs}`,\n );\n\n // Materialize the single static source row. Deferred to a microtask so\n // we run *after* the host installs the releases reverse-RPC handler.\n queueMicrotask(() => {\n void registerSources(params.hostRpc).then((result) => {\n if (result) {\n logger.info(`register_sources: registered=${result.registered} pruned=${result.pruned}`);\n }\n });\n });\n },\n});\n\nlogger.info(\"MangaUpdates release-source plugin started\");\n"],
5
- "mappings": "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",
4
+ "sourcesContent": [null, null, null, null, null, null, null, null, "/**\n * MangaUpdates per-series RSS fetcher.\n *\n * Wraps `fetch` with conditional GET (`If-None-Match` from a stored ETag) and\n * a hard timeout. 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; 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}\n\n/** Public base URL for MangaUpdates' v1 RSS API. */\nexport const MANGAUPDATES_RSS_BASE = \"https://api.mangaupdates.com/v1/series\";\n\n/**\n * Normalize a MangaUpdates series ID to its numeric form for API calls.\n *\n * MangaUpdates uses two interchangeable representations of the same ID:\n *\n * - **Numeric** (e.g. `15180124327`) \u2014 the internal primary key. Every\n * `/v1/series/...` API endpoint requires this form.\n * - **Base36 slug** (e.g. `6z1uqw7`) \u2014 a base36 encoding of the numeric\n * ID, used in public URLs only (`mangaupdates.com/series/6z1uqw7/...`).\n * The API rejects this form with a 405.\n *\n * Metadata sources (MangaBaka, etc.) typically scrape the public URL and\n * store the slug, so the value we receive on `entry.externalIds.mangaupdates`\n * is whatever the source happened to grab. Decode here so callers don't\n * have to know.\n *\n * Returns the input unchanged when it's already an all-digit string;\n * `null` when the input contains characters outside the base36 alphabet\n * (caller should surface as a configuration error).\n */\nexport function normalizeMangaUpdatesId(raw: string): string | null {\n const trimmed = raw.trim();\n if (trimmed.length === 0) return null;\n if (/^\\d+$/.test(trimmed)) return trimmed;\n if (!/^[0-9a-z]+$/i.test(trimmed)) return null;\n // parseInt('6z1uqw7', 36) = 15180124327. JS numbers are precise for\n // integers up to 2^53; MangaUpdates IDs sit well below that.\n const decoded = Number.parseInt(trimmed, 36);\n if (!Number.isFinite(decoded) || decoded <= 0) return null;\n return String(decoded);\n}\n\n/**\n * Build the per-series RSS URL. Accepts either the numeric ID or the\n * base36 slug \u2014 see `normalizeMangaUpdatesId` for the rationale.\n */\nexport function feedUrl(mangaUpdatesId: string): string {\n const normalized = normalizeMangaUpdatesId(mangaUpdatesId) ?? mangaUpdatesId;\n return `${MANGAUPDATES_RSS_BASE}/${normalized}/rss`;\n}\n\n/**\n * Conditional GET against a per-series RSS feed.\n *\n * @param mangaUpdatesId - The MangaUpdates series ID.\n * @param previousEtag - The ETag from the previous successful poll (if any).\n * @param opts - Fetcher options (custom fetch, timeout).\n */\nexport async function fetchSeriesFeed(\n mangaUpdatesId: string,\n previousEtag: string | null,\n opts: FetcherOptions = {},\n): Promise<FetchResult> {\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch;\n const timeoutMs = opts.timeoutMs ?? 10_000;\n\n const url = feedUrl(mangaUpdatesId);\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\n // AbortSignal.timeout is the cleanest path. Falling back to a manual\n // controller would add complexity without value (we already require Node\n // 22+).\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 // Treat aborts and other transport-level failures as 0/unavailable so\n // the host's per-host backoff layer can detect \"this domain is sad\n // right now\" without us having to invent a fake HTTP status.\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 return { kind: \"ok\", body, etag, status: 200 };\n }\n\n // Pass through 429 / 5xx so the host's backoff layer sees the real status.\n return {\n kind: \"error\",\n status: resp.status,\n message: `upstream returned ${resp.status} ${resp.statusText}`,\n };\n}\n", "/**\n * RSS parser for MangaUpdates per-series feeds.\n *\n * Per-series feed: `https://api.mangaupdates.com/v1/series/{series_id}/rss`\n *\n * The v1 RSS feed is intentionally sparse:\n * - `<title>` carries `{Series Name} {v.N}? {c.N}` \u2014 chapter and/or volume\n * suffixed with optional letter (`c.113a`, `c.113b` for split chapters)\n * - `<description>` carries the scanlation group name\n * - per-item `<link>`, `<guid>`, `<pubDate>` are NOT present; only the\n * channel-level `<link>` (the series page on mangaupdates.com) exists\n *\n * Items that carry neither chapter nor volume info are dropped \u2014 they're\n * usually announcements (\"oneshot release\", series-name-only entries) and\n * have no place in an inbox.\n *\n * Implementation note: we do NOT pull in a heavy XML parser. The MangaUpdates\n * RSS format is simple, well-formed, and stable. A small targeted regex\n * pipeline avoids a 100kb dependency and CVE surface for marginal benefit.\n */\n\n/** Parsed item, pre-`ReleaseCandidate`. */\nexport interface ParsedRssItem {\n /** Stable per-source ID. Derived from the release URL or guid. */\n externalReleaseId: string;\n /** Original title string. Useful for debugging / fallback. */\n title: string;\n /** Chapter number (decimals supported, e.g. \"47.5\"). */\n chapter: number | null;\n /** Volume number. */\n volume: number | null;\n /**\n * Language tag (lowercased ISO 639-1). Defaults to `\"en\"` when the title\n * doesn't carry an explicit `(xx)` code, since the MangaUpdates v1 RSS\n * endpoint serves the English release stream. The legacy\n * `UNKNOWN_LANGUAGE` sentinel is still exported for callers that want\n * to surface \"no tag detected\" explicitly, but the parser no longer\n * produces it on its own.\n */\n language: string;\n /** Scanlation group name (best-effort; nullable). */\n group: string | null;\n /** Release page URL on MangaUpdates. Used as `payloadUrl`. */\n link: string;\n /** ISO-8601 string. Falls back to \"now\" when pubDate is missing/invalid. */\n observedAt: string;\n}\n\n/** Sentinel returned when the language tag can't be detected. */\nexport const UNKNOWN_LANGUAGE = \"unknown\" as const;\n\n// -----------------------------------------------------------------------------\n// XML helpers\n// -----------------------------------------------------------------------------\n\n/** Strip CDATA wrapper if present, unescape `&amp;` `&lt;` `&gt;` `&quot;`. */\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 const re = new RegExp(`<${tag}[^>]*>([\\\\s\\\\S]*?)</${tag}>`, \"i\");\n const m = xml.match(re);\n if (!m?.[1]) return null;\n return decodeXmlText(m[1]);\n}\n\n/** Pull all `<item>...</item>` blocks from a feed. */\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 * Extract chapter/volume/group/language from a MangaUpdates RSS title.\n *\n * Observed shapes:\n * \"Vol.2 c.14 by GroupName (en)\"\n * \"v.2 c.14.5 by GroupName (es)\"\n * \"c.143 by GroupName\" (language missing)\n * \"Vol.15 by GroupName (en)\" (volume-only bundle)\n * \"c.143 (en)\" (no group)\n *\n * Volume tokens: `v.N`, `vol.N`, `Vol.N` (case-insensitive).\n * Chapter tokens: `c.N`, `ch.N`, `Ch.N` (decimals allowed).\n * Group: text between `by ` and the next `(` or end-of-string.\n * Language: trailing `(xx)` two-letter code, lowercased.\n */\nexport function parseTitle(title: string): {\n chapter: number | null;\n volume: number | null;\n group: string | null;\n language: string;\n} {\n const trimmed = title.trim();\n\n // Chapter: c.N or ch.N. Decimals (`47.5`) and letter suffixes (`113a`,\n // `113b` for split chapters) are both supported; the letter suffix is\n // stripped so `c.113a` and `c.113b` map to chapter 113. Letter-suffix\n // variants get distinct externalReleaseIds via the group, so they remain\n // separate ledger rows even though they share an integer. The lookahead\n // (`(?![0-9])`) replaces the older `\\b` so the trailing letter doesn't\n // block the match the way `\\b` does between two word characters.\n let chapter: number | null = null;\n const chMatch = trimmed.match(/\\bc(?:h)?\\.?\\s*([0-9]+(?:\\.[0-9]+)?)[a-z]?(?![0-9])/i);\n if (chMatch?.[1]) {\n const n = Number.parseFloat(chMatch[1]);\n if (Number.isFinite(n)) chapter = n;\n }\n\n // Volume: v.N or vol.N. Letter suffixes accepted and discarded for the\n // same reason as chapters.\n let volume: number | null = null;\n const volMatch = trimmed.match(/\\bv(?:ol)?\\.?\\s*([0-9]+)[a-z]?(?![0-9])/i);\n if (volMatch?.[1]) {\n const n = Number.parseInt(volMatch[1], 10);\n if (Number.isFinite(n)) volume = n;\n }\n\n // Group: legacy \"by <Group>\" pattern. The current MangaUpdates v1 RSS\n // feed places the scanlation group in `<description>`, not the title;\n // this branch is kept as a fallback so older / legacy feed shapes still\n // surface a group. Captured up to `(` or end-of-string so a trailing\n // `(en)` language tag doesn't bleed into the group name.\n let group: string | null = null;\n const groupMatch = trimmed.match(/\\bby\\s+(.+?)(?:\\s*\\([a-z]{2,3}\\)\\s*)?$/i);\n if (groupMatch?.[1]) {\n const candidate = groupMatch[1].trim();\n if (candidate.length > 0) group = candidate;\n }\n\n // Language: trailing parenthesized 2-3 letter code (e.g. (en), (es), (id), (por)).\n //\n // The current MangaUpdates v1 RSS endpoint (`/v1/series/{id}/rss`) ships\n // titles without a language tag \u2014 it's the English-localized release\n // stream by design. Default to `\"en\"` so items aren't dropped by the\n // client-side language gate; an explicit `(es)` / `(id)` / etc. still\n // wins when present, and the host's per-series language list remains\n // the authoritative gate downstream. The legacy `UNKNOWN_LANGUAGE`\n // sentinel is kept exported for backwards compatibility but no longer\n // produced by this parser.\n let language = \"en\";\n const langMatch = trimmed.match(/\\(([a-z]{2,3})\\)\\s*$/i);\n if (langMatch?.[1]) {\n language = langMatch[1].toLowerCase();\n }\n\n return { chapter, volume, group, language };\n}\n\n// -----------------------------------------------------------------------------\n// Item parsing\n// -----------------------------------------------------------------------------\n\n/**\n * Best-effort `pubDate` -> ISO-8601 conversion. MangaUpdates uses RFC-2822\n * style dates (`Mon, 04 May 2026 02:31:00 GMT`). Falls back to \"now\" on\n * invalid input \u2014 never throws, since one bad pubDate shouldn't drop the\n * whole feed.\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 * Derive a stable external_release_id.\n *\n * Priority:\n * 1. `<guid>` if present (richest legacy format).\n * 2. `<link>` if present (legacy format with per-item links).\n * 3. Deterministic hash of `(title + group + pubDate)` for the current\n * v1 RSS shape, which carries none of the above per-item fields.\n * Including the group in the hash is what lets multiple groups\n * releasing the same chapter (\"c.200\" by Asura, by FLAME-SCANS,\n * by LeviatanScans) hash to distinct IDs and become distinct\n * ledger rows. Same-group same-chapter re-polls collide on the\n * hash and dedupe, which is what the host expects.\n */\nfunction deriveExternalReleaseId(\n guid: string | null,\n link: string | null,\n title: string,\n group: string | null,\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 const fallback = `${title}|${group ?? \"\"}|${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 MangaUpdates `<item>` block into a `ParsedRssItem`. Returns\n * null when the item is unusable:\n * - missing `<title>` (truly malformed), or\n * - title carries neither chapter nor volume (announcements, oneshot\n * stubs, series-name-only entries \u2014 pure inbox noise).\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 description = extractTagText(itemXml, \"description\");\n\n const { chapter, volume, group: groupFromTitle, language } = parseTitle(title);\n if (chapter === null && volume === null) return null;\n\n // The v1 RSS feed places the scanlation group in `<description>`. Prefer\n // it; fall back to the legacy \"by <Group>\" title pattern.\n const descTrimmed = description?.trim();\n const group = descTrimmed && descTrimmed.length > 0 ? descTrimmed : groupFromTitle;\n\n return {\n externalReleaseId: deriveExternalReleaseId(guid, link, title, group, pubDate),\n title,\n chapter,\n volume,\n group,\n language,\n link: link ?? \"\",\n observedAt: pubDateToIso(pubDate),\n };\n}\n\n/** Parsed feed: items plus the channel-level link (if any). */\nexport interface ParsedFeed {\n /** Channel-level `<link>` \u2014 the series page on mangaupdates.com. Used as\n * the `payloadUrl` for releases when no per-item link exists (the v1\n * RSS shape). `null` when the channel block is missing or malformed. */\n channelLink: string | null;\n items: ParsedRssItem[];\n}\n\n/**\n * Parse a full MangaUpdates per-series RSS feed body. Items that fail\n * `parseItem` (missing title, or no chapter/volume) are dropped silently \u2014\n * the feed parser is best-effort tolerant.\n */\nexport function parseFeed(xml: string): ParsedFeed {\n return {\n channelLink: extractChannelLink(xml),\n items: splitItems(xml)\n .map(parseItem)\n .filter((i): i is ParsedRssItem => i !== null),\n };\n}\n\n/**\n * Extract the channel-level `<link>` from a feed. The v1 RSS feed uses\n * `<channel><link>https://...</link></channel>` and that URL is the series\n * page on mangaupdates.com. We prefer the first `<link>` *outside* any\n * `<item>` block so per-item legacy links (which we don't expect at the\n * channel level anyway) can never bleed in.\n */\nfunction extractChannelLink(xml: string): string | null {\n // Strip every <item>...</item> block before searching \u2014 cheap way to\n // scope to the channel header.\n const stripped = xml.replace(/<item\\b[^>]*>[\\s\\S]*?<\\/item>/gi, \"\");\n const link = extractTagText(stripped, \"link\");\n if (!link) return null;\n const trimmed = link.trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n", "/**\n * Filtering: language allowlist + group blocklist.\n *\n * Filters are applied client-side in the plugin (before recording) for two\n * reasons:\n * 1. Keeps the ledger small. Out-of-language items would be dropped by the\n * host anyway via the latest_known_* gate, but writing them to the\n * ledger pollutes the inbox and wastes write IO.\n * 2. Keeps the inbox clean. Users who configure `[\"en\"]` don't want to see\n * Spanish entries hidden behind a state flag \u2014 they want them gone.\n */\n\nimport { type ParsedRssItem, UNKNOWN_LANGUAGE } from \"./parser.js\";\n\n/**\n * Resolved, normalized filter inputs for a single series. Both lists are\n * lowercased + trimmed. Empty `languages` is interpreted as \"no filter\"\n * (everything passes), but the caller is expected to pass at least the\n * server-wide default to avoid that footgun.\n */\nexport interface ResolvedFilters {\n /** Lowercased ISO 639-1 codes; empty = no filter. */\n languages: string[];\n /** Lowercased group names; case-insensitive exact match against `group`. */\n blockedGroups: Set<string>;\n /**\n * Whether to include items whose language couldn't be detected\n * (`UNKNOWN_LANGUAGE` sentinel). Default false \u2014 be conservative.\n */\n includeUnknownLanguage: boolean;\n}\n\n/**\n * Build resolved filters from raw config strings + lists. Centralizes the\n * normalization so the poll handler doesn't have to care about casing or\n * whitespace.\n */\nexport function resolveFilters(input: {\n languages: string[];\n blockedGroups: string[];\n includeUnknownLanguage?: boolean;\n}): ResolvedFilters {\n const languages = dedupePreserveOrder(\n input.languages.map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0),\n );\n const blockedGroups = new Set(\n input.blockedGroups.map((s) => s.trim().toLowerCase()).filter((s) => s.length > 0),\n );\n return {\n languages,\n blockedGroups,\n includeUnknownLanguage: input.includeUnknownLanguage ?? false,\n };\n}\n\n/**\n * Parse a comma-separated string into a clean list (trim, drop empties).\n * Helper for `blockedGroups` which is admin-config typed as a single string.\n */\nexport function parseCommaList(raw: unknown): string[] {\n if (typeof raw !== \"string\") return [];\n return raw\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s.length > 0);\n}\n\n/**\n * Returns true if the item should be kept.\n *\n * Language filter:\n * - If `languages` is empty \u2192 pass.\n * - Otherwise, item.language must be in the list (case-insensitive).\n * - `unknown` language is rejected unless `includeUnknownLanguage` is true.\n *\n * Group filter:\n * - If `group` is null \u2192 pass (we have nothing to match against).\n * - Otherwise, group must NOT be in `blockedGroups`.\n */\nexport function passesFilters(item: ParsedRssItem, filters: ResolvedFilters): boolean {\n // Language gate.\n if (item.language === UNKNOWN_LANGUAGE) {\n if (!filters.includeUnknownLanguage) return false;\n } else if (filters.languages.length > 0) {\n if (!filters.languages.includes(item.language.toLowerCase())) return false;\n }\n\n // Group blocklist.\n if (item.group !== null && filters.blockedGroups.size > 0) {\n if (filters.blockedGroups.has(item.group.trim().toLowerCase())) return false;\n }\n\n return true;\n}\n\nfunction dedupePreserveOrder(xs: string[]): string[] {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const x of xs) {\n if (!seen.has(x)) {\n seen.add(x);\n out.push(x);\n }\n }\n return out;\n}\n", "{\n \"name\": \"@ashdev/codex-plugin-release-mangaupdates\",\n \"version\": \"1.21.1\",\n \"description\": \"MangaUpdates RSS release-source plugin for Codex - announces new chapter releases for tracked series in user-configured languages\",\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-mangaupdates\"\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 \"mangaupdates\",\n \"release-source\",\n \"manga\"\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/**\n * External-ID source name for MangaUpdates.\n *\n * MangaUpdates IDs are populated by metadata-provider plugins (e.g.\n * MangaBaka cross-references) or pasted manually by the user via the series\n * tracking panel. The release plugin needs the bare source name (no\n * `plugin:` prefix) here to match the host's external-ID filter.\n */\nexport const EXTERNAL_ID_SOURCE_MANGAUPDATES = \"mangaupdates\" as const;\n\nexport const manifest = {\n name: \"release-mangaupdates\",\n displayName: \"MangaUpdates Releases\",\n version: packageJson.version,\n description:\n \"Announces new chapter releases for tracked series via MangaUpdates per-series RSS feeds. Filters by user-configured languages.\",\n author: \"Codex\",\n homepage: \"https://github.com/AshDevFr/codex\",\n protocolVersion: \"1.1\",\n capabilities: {\n releaseSource: {\n kinds: [\"rss-series\"],\n requiresAliases: false,\n requiresExternalIds: [EXTERNAL_ID_SOURCE_MANGAUPDATES],\n canAnnounceChapters: true,\n canAnnounceVolumes: true,\n },\n },\n configSchema: {\n description:\n \"MangaUpdates plugin configuration. Per-series language preferences live on each series' tracking config; the values here are server-wide defaults applied when a series doesn't override them.\",\n fields: [\n {\n key: \"blockedGroups\",\n label: \"Blocked Scanlation Groups\",\n description:\n \"Comma-separated list of scanlation group names to exclude from announcements (case-insensitive, exact match). Per-series overrides may further extend this list.\",\n type: \"string\" as const,\n required: false,\n default: \"\",\n example: \"LowQualityScans,MTL Group\",\n },\n {\n key: \"requestTimeoutMs\",\n label: \"Request Timeout (ms)\",\n description:\n \"How long to wait for a single RSS fetch before giving up. Defaults to 10000 (10 seconds).\",\n type: \"number\" as const,\n required: false,\n default: 10_000,\n },\n ],\n },\n userDescription:\n \"Announces new chapters for series you've tracked, using their MangaUpdates IDs. Filters releases to languages you can read. Notification-only \u2014 Codex does not download anything.\",\n adminSetupInstructions:\n \"1. No config is required to get started \u2014 saving the plugin is enough. The plugin auto-registers a single source row (`MangaUpdates Releases`) in **Settings \u2192 Release tracking** on first start, where you can disable it, change the poll interval, or hit *Poll now*. 2. To get announcements for a series, edit its tracking panel and either paste a `mangaupdates` external ID or let the metadata-refresh path populate it from MangaBaka cross-references. 3. Optional: set `blockedGroups` (CSV, case-insensitive) to filter noisy scanlators server-wide; per-series language preferences live on each series' tracking config and override the server default (`release_tracking.default_languages`). No credentials are needed; MangaUpdates RSS feeds are public.\",\n} as const satisfies PluginManifest & {\n capabilities: { releaseSource: { kinds: [\"rss-series\"] } };\n};\n", "/**\n * MangaUpdates RSS Release-Source Plugin for Codex.\n *\n * Polls per-series RSS feeds at MangaUpdates and announces new chapter /\n * volume releases for tracked series. The plugin is the first writer of\n * `release_ledger` rows in production \u2014 earlier phases build the\n * infrastructure, this one delivers the first real notification feed.\n *\n * Flow per `releases/poll`:\n * 1. Pull tracked-series scope from the host (`releases/list_tracked`).\n * Filtered server-side to series with a `mangaupdates` external ID.\n * 2. For each series, conditional GET the RSS feed.\n * 3. Parse the response into items, then filter by:\n * - per-series language list (admin / per-series config)\n * - admin-configured group blocklist\n * 4. Build `ReleaseCandidate` rows and stream them via\n * `releases/record`. The host's matcher applies the threshold and\n * ledger dedup.\n * 5. Pass the new ETag back via the poll response so the host updates\n * the source row.\n *\n * **Concurrency note:** The plugin host already serializes RPCs per plugin\n * process, so we don't need to throttle internally beyond an in-poll loop\n * that walks tracked series sequentially.\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 { fetchSeriesFeed } from \"./fetcher.js\";\nimport { parseCommaList, passesFilters, resolveFilters } from \"./filter.js\";\nimport { EXTERNAL_ID_SOURCE_MANGAUPDATES, manifest } from \"./manifest.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 /** Admin-configured group blocklist (lowercased exact match). */\n blockedGroupsCsv: string;\n /** Hard timeout for upstream fetches. */\n requestTimeoutMs: number;\n}\n\nconst state: PluginState = {\n hostRpc: null,\n blockedGroupsCsv: \"\",\n requestTimeoutMs: 10_000,\n};\n\n/** Reset state. Exported for tests; not part of the plugin contract. */\nexport function _resetState(): void {\n state.hostRpc = null;\n state.blockedGroupsCsv = \"\";\n state.requestTimeoutMs = 10_000;\n}\n\n// =============================================================================\n// Reverse-RPC wrappers (typed shorthands so the poll code reads cleanly)\n// =============================================================================\n\ninterface ListTrackedResponse {\n tracked: TrackedSeriesEntry[];\n nextOffset?: number;\n}\n\ninterface RecordResponse {\n ledgerId: string;\n deduped: boolean;\n}\n\ninterface CountTrackedResponse {\n total: number;\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\n/**\n * Total tracked-series denominator for this source, scoped by the\n * plugin's `requires_external_ids` manifest declaration. Returns `null`\n * when the host doesn't know the method (older host build) \u2014 callers\n * fall back to progressive denominator emits in that case.\n */\nasync function countTracked(rpc: HostRpcClient, sourceId: string): Promise<number | null> {\n try {\n const r = await rpc.call<CountTrackedResponse>(RELEASES_METHODS.COUNT_TRACKED, {\n sourceId,\n });\n return r.total;\n } catch (err) {\n if (err instanceof HostRpcError && err.code === -32601) {\n // Host doesn't know `count_tracked` \u2014 older build. Degrade silently.\n return null;\n }\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(`count_tracked failed for ${sourceId}: ${reason}`);\n return null;\n }\n}\n\n/**\n * Best-effort progress emit. Failures are swallowed \u2014 progress is a\n * UX nice-to-have, never a reason to abort a poll.\n */\nasync function reportProgress(\n rpc: HostRpcClient,\n current: number,\n total: number,\n message?: string,\n): Promise<void> {\n try {\n await rpc.call(\n RELEASES_METHODS.REPORT_PROGRESS,\n message !== undefined ? { current, total, message } : { current, total },\n );\n } catch (err) {\n if (err instanceof HostRpcError && err.code === -32601) {\n // Older host without progress support \u2014 silently drop.\n return;\n }\n const reason = err instanceof Error ? err.message : String(err);\n logger.debug(`report_progress dropped: ${reason}`);\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 // Threshold rejection / validation error / unknown source. Log and\n // skip; the next poll will retry the still-eligible candidates.\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 * Lazily walk all tracked-series pages from the host. Yields entries one\n * series at a time so the caller can interleave per-series fetches without\n * buffering the whole list (relevant for users tracking hundreds of series).\n */\nasync function* iterateTrackedSeries(\n rpc: HostRpcClient,\n sourceId: string,\n): AsyncGenerator<TrackedSeriesEntry> {\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 yield entry;\n }\n if (page.nextOffset === undefined || page.tracked.length === 0) return;\n offset = page.nextOffset;\n }\n}\n\n/**\n * Per-series effective language list. We use the host's `latestKnown*`\n * exposure plus the `externalIds` map to scope the fetch, but the\n * languages config is owned by the host (set on `series_tracking.languages`\n * with fallback to the server-wide default).\n *\n * However, the current `releases/list_tracked` response shape doesn't\n * expose per-series `languages` \u2014 see plan doc for this design choice.\n * For Phase 6 the plugin reads its admin-level group blocklist and emits\n * candidates with the language tag from the parsed entry; the host's\n * `latest_known_*` advance gate enforces the per-series language list\n * authoritatively (see `services/release/languages.rs`).\n *\n * We *also* want to drop out-of-language candidates client-side to keep the\n * ledger small and the inbox clean. Without per-series languages on the\n * tracked-series payload, the client-side filter degrades to a no-op\n * pass-everything for known languages \u2014 leaving it to the host's gate. The\n * group blocklist still applies.\n *\n * If a future protocol revision exposes `effectiveLanguages` on the\n * tracked-series entry, swap this stub for the real list and the existing\n * `passesFilters` will do the right thing.\n */\nfunction effectiveLanguagesForSeries(_entry: TrackedSeriesEntry): string[] {\n return []; // empty = no client-side language gate; host gate is authoritative\n}\n\n/**\n * Map a `ParsedRssItem` to a `ReleaseCandidate`. Confidence is 1.0 because\n * the match is keyed by external ID \u2014 there's no fuzzy matching.\n *\n * `payloadUrl` priority: per-item link (legacy feed shape) \u2192 channel-level\n * series page link (current v1 RSS shape) \u2192 last-resort `urn:mu:` URN. The\n * URN fallback should never fire in practice; it exists so a malformed\n * feed without even a channel link doesn't break the host's non-empty\n * `payload_url` invariant.\n */\nfunction toCandidate(\n entry: TrackedSeriesEntry,\n item: ParsedRssItem,\n channelLink: string | null,\n): ReleaseCandidate {\n const payloadUrl =\n item.link.length > 0\n ? item.link\n : channelLink && channelLink.length > 0\n ? channelLink\n : `urn:mu:${item.externalReleaseId}`;\n const candidate: ReleaseCandidate = {\n seriesMatch: {\n codexSeriesId: entry.seriesId,\n confidence: 1.0,\n reason: `mangaupdates_id:${entry.externalIds?.[EXTERNAL_ID_SOURCE_MANGAUPDATES] ?? \"\"}`,\n },\n externalReleaseId: item.externalReleaseId,\n // MangaUpdates always reports a single chapter / volume per release.\n // Wrap as one-element span lists for the new SDK shape; `null` when\n // the parser didn't see a value at all.\n volumes: item.volume === null ? null : [{ start: item.volume, end: item.volume }],\n chapters: item.chapter === null ? null : [{ start: item.chapter, end: item.chapter }],\n language: item.language,\n groupOrUploader: item.group,\n payloadUrl,\n observedAt: item.observedAt,\n };\n return candidate;\n}\n\n// =============================================================================\n// Per-series poll\n// =============================================================================\n\n/** Outcome of a single per-series fetch+record cycle. */\nexport interface SeriesPollOutcome {\n seriesId: string;\n fetched: boolean;\n notModified: boolean;\n parsed: number;\n /** Of those parsed, how many passed client-side filters and were sent to record. */\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 if the per-series fetch failed; empty otherwise. */\n error: string;\n}\n\n/**\n * Poll a single series. Internal \u2014 exposed for testing.\n *\n * Aggregates the worst (highest) upstream status across the per-series\n * fetches at the call site so the host's per-host backoff layer sees real\n * 429/5xx signals.\n */\nexport async function pollSeries(\n rpc: HostRpcClient,\n sourceId: string,\n entry: TrackedSeriesEntry,\n options: {\n blockedGroups: string[];\n timeoutMs: number;\n fetchImpl?: typeof fetch;\n },\n): Promise<SeriesPollOutcome> {\n const muId = entry.externalIds?.[EXTERNAL_ID_SOURCE_MANGAUPDATES];\n if (!muId) {\n return {\n seriesId: entry.seriesId,\n fetched: false,\n notModified: false,\n parsed: 0,\n matched: 0,\n recorded: 0,\n deduped: 0,\n upstreamStatus: 0,\n etag: null,\n error: \"missing mangaupdates external ID\",\n };\n }\n\n // We don't have per-series ETag here \u2014 that lives on the source row, not\n // the series. For a per-source feed (rss-uploader) ETags align cleanly;\n // for per-series feeds (this plugin) we'd need per-(source, series) state\n // to do conditional GETs per series. That's a future optimization; for\n // now we always do an unconditional GET. Daily polls + small per-series\n // bodies keep the bandwidth cost negligible.\n const result = await fetchSeriesFeed(muId, null, {\n fetchImpl: options.fetchImpl,\n timeoutMs: options.timeoutMs,\n });\n\n if (result.kind === \"notModified\") {\n return {\n seriesId: entry.seriesId,\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 seriesId: entry.seriesId,\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, channelLink } = parseFeed(result.body);\n const filters = resolveFilters({\n languages: effectiveLanguagesForSeries(entry),\n blockedGroups: options.blockedGroups,\n });\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n for (const item of items) {\n if (!passesFilters(item, filters)) continue;\n matched++;\n const candidate = toCandidate(entry, item, channelLink);\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 seriesId: entry.seriesId,\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 * Top-level poll handler. Exported for tests (no underscore prefix because\n * it's actually a load-bearing function that just happens to live behind\n * the SDK plugin wrapper at module scope; `_resetState` is the\n * pattern for state-only test seams).\n */\nexport async function poll(\n params: ReleasePollRequest,\n rpc: HostRpcClient,\n): Promise<ReleasePollResponse> {\n const sourceId = params.sourceId;\n const blockedGroups = parseCommaList(state.blockedGroupsCsv);\n\n // Pre-count so progress emits can carry a stable denominator. Falls\n // back to progressive ('N polled' with no total) when the host doesn't\n // implement count_tracked, keeping us forward-compatible.\n const total = await countTracked(rpc, sourceId);\n\n let parsed = 0;\n let matched = 0;\n let recorded = 0;\n let deduped = 0;\n let worstStatus = 200;\n let lastEtag: string | null = null;\n let seenSeries = 0;\n // Series the host returned that lack a MangaUpdates external ID. A high\n // count here is the most common cause of an \"empty\" poll: the plugin\n // can't fetch a feed without an MU ID, so the user needs to populate\n // those (manual paste or metadata refresh from MangaBaka).\n let skippedNoMuId = 0;\n\n for await (const entry of iterateTrackedSeries(rpc, sourceId)) {\n seenSeries++;\n const outcome = await pollSeries(rpc, sourceId, entry, {\n blockedGroups,\n timeoutMs: state.requestTimeoutMs,\n });\n parsed += outcome.parsed;\n matched += outcome.matched;\n recorded += outcome.recorded;\n deduped += outcome.deduped;\n if (outcome.upstreamStatus > worstStatus) {\n worstStatus = outcome.upstreamStatus;\n }\n if (outcome.etag) lastEtag = outcome.etag;\n\n if (outcome.error === \"missing mangaupdates external ID\") {\n skippedNoMuId++;\n } else if (outcome.error) {\n logger.warn(`series ${entry.seriesId}: ${outcome.error} (status ${outcome.upstreamStatus})`);\n }\n\n // Progress: rate-limited host-side, so it's OK to fire every iteration.\n // When `total` is unknown, send seenSeries as both current and total\n // so the host emits the message without a useful percentage.\n await reportProgress(\n rpc,\n seenSeries,\n total ?? seenSeries,\n `Polled ${seenSeries}${total !== null ? `/${total}` : \"\"} series`,\n );\n }\n\n if (skippedNoMuId > 0) {\n logger.info(\n `skipped ${skippedNoMuId} of ${seenSeries} tracked series for source=${sourceId}: no mangaupdates external ID. Add one in the Tracking panel or run a metadata refresh.`,\n );\n }\n\n logger.info(\n `poll complete: source=${sourceId} series=${seenSeries} skipped=${skippedNoMuId} parsed=${parsed} matched=${matched} recorded=${recorded} deduped=${deduped} worst_status=${worstStatus}`,\n );\n\n // Report counters back to the host so the source's `last_summary` is\n // accurate. Without these the host only sees the (empty) `candidates`\n // payload \u2014 we record via reverse-RPC mid-poll \u2014 and the badge reads\n // \"Fetched 0 items\" no matter what actually happened.\n // Per-series ETags don't align with the per-source state slot, so we\n // intentionally leave `etag` undefined unless we actually saw one\n // (which today we won't, since we don't pass If-None-Match per series).\n return {\n notModified: false,\n upstreamStatus: worstStatus,\n parsed,\n matched,\n recorded,\n deduped,\n ...(lastEtag !== null ? { etag: lastEtag } : {}),\n };\n}\n\n// =============================================================================\n// Plugin Initialization\n// =============================================================================\n\n/**\n * Register a single static source row representing the MangaUpdates batch\n * feed. Unlike Nyaa (one row per uploader), MangaUpdates polls all tracked\n * series under one logical feed, so we always declare exactly one row keyed\n * `default`. Retries on `METHOD_NOT_FOUND` to handle the brief race where\n * the host has not yet installed the releases reverse-RPC handler.\n */\nexport async function registerSources(\n rpc: HostRpcClient,\n): Promise<{ registered: number; pruned: number } | null> {\n const sources = [\n {\n sourceKey: \"default\",\n displayName: \"MangaUpdates Releases\",\n kind: \"rss-series\" as const,\n config: null,\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 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\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 if (typeof ac.blockedGroups === \"string\") {\n state.blockedGroupsCsv = ac.blockedGroups;\n }\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 logger.info(\n `initialized: blockedGroups=${state.blockedGroupsCsv ? \"set\" : \"empty\"} timeoutMs=${state.requestTimeoutMs}`,\n );\n\n // Materialize the single static source row. Deferred to a microtask so\n // we run *after* the host installs the releases reverse-RPC handler.\n queueMicrotask(() => {\n void registerSources(params.hostRpc).then((result) => {\n if (result) {\n logger.info(`register_sources: registered=${result.registered} pruned=${result.pruned}`);\n }\n });\n });\n },\n});\n\nlogger.info(\"MangaUpdates 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-mangaupdates",
3
- "version": "1.20.0",
3
+ "version": "1.21.1",
4
4
  "description": "MangaUpdates RSS release-source plugin for Codex - announces new chapter releases for tracked series in user-configured languages",
5
5
  "main": "dist/index.js",
6
6
  "bin": "dist/index.js",
@@ -39,7 +39,7 @@
39
39
  "node": ">=22.0.0"
40
40
  },
41
41
  "dependencies": {
42
- "@ashdev/codex-plugin-sdk": "^1.20.0"
42
+ "@ashdev/codex-plugin-sdk": "^1.21.1"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@biomejs/biome": "^2.4.4",