diff --git a/plugins/arabic/galaxynovels.ts b/plugins/arabic/galaxynovels.ts index 9be591784..8c7441083 100644 --- a/plugins/arabic/galaxynovels.ts +++ b/plugins/arabic/galaxynovels.ts @@ -11,7 +11,7 @@ type ChapterJSON = { label: string; title: string; url: string; - content_api: string; + content_api?: string; date_iso: string; }; @@ -21,10 +21,23 @@ type ChaptersIndex = { chapters: ChapterJSON[]; }; +type ChaptersManifest = { + novel_id: number; + total: number; + pack_url: string; + live_tail: ChapterJSON[]; +}; + +type ChaptersPack = { + novel_id: number; + total: number; + chapters: ChapterJSON[]; +}; + class GalaxyNovels implements Plugin.PluginBase { id = 'galaxynovels'; name = 'Galaxy Novels'; - version = '1.1.1'; + version = '1.1.2'; icon = 'src/ar/galaxynovels/icon.png'; site = 'https://galaxynovels.com/'; @@ -58,6 +71,66 @@ class GalaxyNovels implements Plugin.PluginBase { return url.startsWith('http') ? new URL(url).pathname : url; } + private toChapterItem( + ch: ChapterJSON, + novelPath: string, + ): Plugin.ChapterItem { + return { + name: ch.label + (ch.title ? `: ${ch.title}` : ''), + path: this.toChapterPath(ch.url) || `${novelPath}chapter-${ch.id}/`, + chapterNumber: ch.position, + releaseTime: ch.date_iso?.split('T')[0] || '', + }; + } + + // The novel page only renders the latest 30 chapters (data-per-page="30") + // and the data-index-url endpoint is currently empty, so the full list is + // read from the static chapter manifest instead: the manifest points at a + // pack file holding every chapter, plus a live_tail with the newest ones. + // The tail is merged over the pack the way the site's own reader does, so + // chapters published after the pack was generated are included. Anything + // missing or malformed here returns null so parseNovel falls back to the + // page-derived list instead of an empty chapter list. + private async fetchManifestChapters( + manifestUrl: string | undefined, + novelPath: string, + ): Promise { + if (!manifestUrl) return null; + try { + const manifestEndpoint = manifestUrl.startsWith('http') + ? manifestUrl + : `${this.baseUrl}${manifestUrl}`; + const manifest = await this.fetchJson(manifestEndpoint); + if (!manifest?.pack_url) return null; + + const packEndpoint = manifest.pack_url.startsWith('http') + ? manifest.pack_url + : `${this.baseUrl}${manifest.pack_url}`; + const pack = await this.fetchJson(packEndpoint); + if (!pack || !Array.isArray(pack.chapters)) return null; + + const byId = new Map(); + for (const ch of pack.chapters) { + if (ch && typeof ch.id === 'number' && ch.url) byId.set(ch.id, ch); + } + const liveTail = Array.isArray(manifest.live_tail) + ? manifest.live_tail + : []; + for (const ch of liveTail) { + if (ch && typeof ch.id === 'number' && ch.url) byId.set(ch.id, ch); + } + + const merged = Array.from(byId.values()) + .filter(ch => typeof ch.position === 'number') + .sort((a, b) => a.position - b.position); + if (merged.length === 0) return null; + + return merged.map(ch => this.toChapterItem(ch, novelPath)); + } catch { + return null; + } + } + private async fetchHtml(url: string): Promise { const res = await fetchApi(url); if (!res.ok) { @@ -145,22 +218,26 @@ class GalaxyNovels implements Plugin.PluginBase { const chaptersContainer = $('[data-wor-chapters-container]'); const chaptersIndexUrl = chaptersContainer.attr('data-index-url'); + const chaptersManifestUrl = chaptersContainer.attr('data-manifest-url'); let chapters: Plugin.ChapterItem[] = []; - if (chaptersIndexUrl) { + const manifestChapters = await this.fetchManifestChapters( + chaptersManifestUrl, + novelPath, + ); + if (manifestChapters && manifestChapters.length > 0) { + chapters = manifestChapters; + } + + if (chapters.length === 0 && chaptersIndexUrl) { try { const indexUrl = chaptersIndexUrl.startsWith('http') ? chaptersIndexUrl : `${this.baseUrl}${chaptersIndexUrl}`; const index = await this.fetchJson(indexUrl); - chapters = index.chapters.map(ch => ({ - name: ch.label + (ch.title ? `: ${ch.title}` : ''), - path: this.toChapterPath(ch.url) || `${novelPath}chapter-${ch.id}/`, - chapterNumber: ch.position, - releaseTime: ch.date_iso?.split('T')[0] || '', - })); + chapters = index.chapters.map(ch => this.toChapterItem(ch, novelPath)); } catch { // fallback to HTML parsing }