diff --git a/plugins/arabic/galaxynovels.ts b/plugins/arabic/galaxynovels.ts index 155c1dae7..26de61e20 100644 --- a/plugins/arabic/galaxynovels.ts +++ b/plugins/arabic/galaxynovels.ts @@ -33,7 +33,7 @@ type ChapterContentResponse = { class GalaxyNovels implements Plugin.PluginBase { id = 'galaxynovels'; name = 'Galaxy Novels'; - version = '1.1.0'; + version = '1.2.0'; icon = 'src/ar/galaxynovels/icon.png'; site = 'https://galaxynovels.com/'; @@ -62,8 +62,13 @@ class GalaxyNovels implements Plugin.PluginBase { private baseUrl = 'https://galaxynovels.com'; + private static readonly BROWSER_UA = + 'Mozilla/5.0 (Linux; Android 13; SM-G991B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36'; + private async fetchHtml(url: string): Promise { - const res = await fetchApi(url); + const res = await fetchApi(url, { + headers: { 'User-Agent': GalaxyNovels.BROWSER_UA }, + }); if (!res.ok) { throw new Error(`Could not reach site (${res.status})`); } @@ -71,7 +76,9 @@ class GalaxyNovels implements Plugin.PluginBase { } private async fetchJson(url: string): Promise { - const res = await fetchApi(url); + const res = await fetchApi(url, { + headers: { 'User-Agent': GalaxyNovels.BROWSER_UA }, + }); if (!res.ok) { throw new Error(`Could not reach site (${res.status})`); } @@ -106,8 +113,7 @@ class GalaxyNovels implements Plugin.PluginBase { const coverLink = $el.find('a.wor-novel-card__cover'); const href = coverLink.attr('href'); const img = $el.find('img.wor-cover-img'); - const cover = - img.attr('data-src') || img.attr('src') || undefined; + const cover = img.attr('data-src') || img.attr('src') || undefined; const title = $el.find('h3 a').text().trim(); if (!href || !title) return; @@ -174,8 +180,12 @@ class GalaxyNovels implements Plugin.PluginBase { if (chapters.length === 0) { $('article.wor-novel-chapter-item').each((_, el) => { const $el = $(el); - const chapterLink = $el.find('h3 a').attr('href') || $el.find('a.wor-novel-chapter-item__num').attr('href'); - const chapterName = $el.find('h3 a').text().trim() || $el.find('a.wor-novel-chapter-item__num').text().trim(); + const chapterLink = + $el.find('h3 a').attr('href') || + $el.find('a.wor-novel-chapter-item__num').attr('href'); + const chapterName = + $el.find('h3 a').text().trim() || + $el.find('a.wor-novel-chapter-item__num').text().trim(); const chapterId = $el.attr('data-chapter-id'); const timeEl = $el.find('time'); const releaseTime = timeEl.attr('datetime')?.split('T')[0] || ''; @@ -228,8 +238,9 @@ class GalaxyNovels implements Plugin.PluginBase { const url = `${this.baseUrl}${chapterPath}`; const html = await this.fetchHtml(url); const $ = loadCheerio(html); - const content = - $('article.wor-chapter-content, .wor-chapter-text, .entry-content').html(); + const content = $( + 'article.wor-chapter-content, .wor-chapter-text, .entry-content', + ).html(); return content || '

Content not available.

'; } @@ -254,9 +265,7 @@ class GalaxyNovels implements Plugin.PluginBase { const term = searchTerm.toLowerCase(); const filtered = searchIndex.items.filter( - n => - n.t.toLowerCase().includes(term) || - n.s.toLowerCase().includes(term), + n => n.t.toLowerCase().includes(term) || n.s.toLowerCase().includes(term), ); const limit = 20; @@ -265,9 +274,7 @@ class GalaxyNovels implements Plugin.PluginBase { return filtered.slice(offset, offset + limit).map(novel => ({ name: novel.t, path: novel.u, - cover: novel.c.startsWith('http') - ? novel.c - : `${this.baseUrl}${novel.c}`, + cover: novel.c.startsWith('http') ? novel.c : `${this.baseUrl}${novel.c}`, })); } } diff --git a/tests/fixtures/2442/chapter-api.json b/tests/fixtures/2442/chapter-api.json new file mode 100644 index 000000000..f3793eb91 --- /dev/null +++ b/tests/fixtures/2442/chapter-api.json @@ -0,0 +1,41 @@ +{ + "schema": 1, + "generated": 1786291239, + "data": { + "id": 221415, + "novel_id": 219910, + "number": "1", + "label": "الفصل 1", + "title": "الفصل 1: هل يجب أن تكوني بهذه القسوة؟", + "display_title": "الفصل 1: هل يجب أن تكوني بهذه القسوة؟", + "position": 1, + "total": 344, + "percent": "0.3%", + "url": "/novel/i-can-practice-the-god-level-system-with-one-click/chapter-1/%d8%a7%d9%84%d9%81%d8%b5%d9%84-1-%d9%87%d9%84-%d9%8a%d8%ac%d8%a8-%d8%a3%d9%86-%d8%aa%d9%83%d9%88%d9%86%d9%8a-%d8%a8%d9%87%d8%b0%d9%87-%d8%a7%d9%84%d9%82%d8%b3%d9%88%d8%a9%d8%9f/", + "api": "/wp-json/wor-reader-app/v1/chapters/221415", + "published_at": "2026-07-24T20:00:00+00:00", + "updated_at": "2026-07-24T20:00:00+00:00", + "novel": { + "id": 219910, + "title": "يمكنني ممارسة الزراعة الروحية بالنظام الاعظم بنقرة واحدة", + "url": "/novel/i-can-practice-the-god-level-system-with-one-click/", + "cover": { + "thumbnail": "/wp-content/uploads/2026/07/h237-150x150.webp", + "medium": "/wp-content/uploads/2026/07/h237-169x300.webp" + } + }, + "navigation": { + "previous_id": 0, + "next_id": 221416, + "previous_url": "", + "next_url": "/novel/i-can-practice-the-god-level-system-with-one-click/chapter-2/%d8%a7%d9%84%d9%81%d8%b5%d9%84-2-%d9%87%d8%b0%d8%a7-%d8%a8%d8%a7%d9%84%d8%aa%d8%a3%d9%83%d9%8a%d8%af-%d8%a3%d8%b1%d9%88%d8%b9-%d9%86%d8%b8%d8%a7%d9%85/", + "previous_api": "", + "next_api": "/wp-json/wor-reader-app/v1/chapters/221416" + }, + "reading": { + "word_count": 637, + "minutes": 3 + }, + "content_html": "

الفصل 1: هل يجب أن تكوني بهذه القسوة؟

\n

“سعال! سعال!”

\n

“تبًا، أين أنا الآن؟”

\n

“ألم أمت بعدما صدمتني سيارة كبيرة؟”

\n

“دهستني شاحنة ثقيلة، ومع ذلك ما زلت حيًا؟”

\n

سعل سو فنغ بعنف مرتين، ثم استيقظ من الظلام

\n

اكتشف أن جسده كان شبه فاقد للإحساس، وأن رجلًا يرتدي ثوبًا قماشيًا قديم الطراز يحمله، وكان يهزه بشدة حتى سال لعابه

\n

وفجأة

\n

دوي!

\n

شعر سو فنغ فجأة بصدمة في دماغه، كأنه انفجر

\n

اجتاحه ألم شديد مصحوب بسيل من الذكريات المتناثرة

\n

“ماذا؟”

\n

“قارة الروح العميقة، مدينة ووتونغ؟”

\n

“هذه ليست الأرض؟ بل عالم خيالي روحاني مهيب للغاية، يحكمه الأقوياء؟”

\n

“هل انتقلت إلى عالم آخر؟ واستوليت على جسد هذا المنحوس الميت الذي يحمل الاسم نفسه، ثم عدت إلى الحياة!؟”

\n

“…”

\n

تركت شظايا الذكريات المختلفة التي ظهرت في ذهنه سو فنغ مذهولًا، بينما هاجت مشاعره كعاصفة عنيفة

\n

وفي تلك اللحظة

\n

“أوه!”

\n

“لقد استيقظت! هذا رائع!”

\n

“لقد أمرت الآنسة يو إر تحديدًا بأن تمزقك الوحوش الشرسة وتلتهمك حيًا، فلن يكون الأمر ممتعًا إن مت قبل ذلك!”

\n

لاحظ الرجل ذو الثوب القماشي حالة سو فنغ الغريبة، فقال بضع كلمات بابتسامة ساخرة

\n

“مـ… ماذا!؟”

\n

“ستلتهمني الوحوش الشرسة حيًا، قضمة بعد قضمة!؟”

\n

فهم سو فنغ كلمات الرجل ذي الثوب القماشي بفضل شظايا الذكريات، فشعر برعب فوري وسرت برودة في جسده كله!

\n

وفي الوقت نفسه، رأى المزيد من المشاهد عبر شظايا الذكريات، وفهم القصة كاملة!

\n

اتضح أن ذلك المنحوس كان يمتلك دون علمه سلالة خاصة تُدعى «سلالة لوتس القمر اللازوردي النقي»، لكن الآخرين اكتشفوا ذلك قبله

\n

وقد دفع هذا بعض الناس إلى الطمع فيها، وبالتحديد الآنسة يو إر التي ذكرها الرجل ذو الثوب القماشي!

\n

كان الاسم الكامل للآنسة يو إر هو شانغقوان يو إر، وقد كسبت ثقة ذلك المنحوس بتظاهرها بالمودة، ثم دبرت خطة لنقل سلالته إلى جسدها

\n

ولخداع ذلك المنحوس، كادت شانغقوان يو إر تبذل كل ما لديها لإقناعه، ودفعت ثمنًا باهظًا، لذلك أخفت في قلبها كراهية شديدة له

\n

وبعد نجاحها، انقلبت شانغقوان يو إر عليه فورًا، وأرادت قتله مباشرة، لكنها شعرت أن ذلك لن يشفي غضبها

\n

لذلك أرسلت الرجل ذا الثوب القماشي، فان مان، ليأخذ ذلك المنحوس إلى الغابة خارج المدينة ويلقيه طعامًا للوحوش الشرسة!

\n

كان ذلك المنحوس ضعيفًا للغاية، وحين غمره الحزن والرعب مات فجأة، فاستولى سو فنغ المنتقل إلى هذا العالم على جسده

\n

“هذا يعني إذًا…”

\n

“أنني أُحمل الآن لأصبح طعامًا للوحوش الشرسة!؟”

\n

“تبًا، لماذا كل هذه القسوة؟ لم يمض على انتقالي إلى هذا العالم حتى دقيقة واحدة!”

\n

أدرك سو فنغ محنته تمامًا، فأطلق السباب بصوت مرتفع وشعر بذعر شديد

\n

كانت الوحوش الشرسة أشد ضراوة بكثير من أي حيوان بري على الأرض، وكان الموت بين أنياب أحدها بالتأكيد من أكثر طرق الموت رعبًا

\n

دفعته رغبته في النجاة إلى التفكير فورًا في وسيلة للهرب

\n

لكنه سرعان ما شعر بشيء من اليأس

\n

فهذا الجسد فقد وعيه مؤقتًا بسبب نقل سلالته!

\n

وفي هذه الحالة، لم يكن الهرب سوى حلم مستحيل!

\n

“لقد انتهى أمري حقًا!”

\n

“لماذا حظي سيئ إلى هذا الحد!؟”

\n

“انتقلت إلى عالم آخر لأموت مجددًا بدلًا من هذا المنحوس، من دون أن أعيش حتى يومًا واحدًا!؟”

\n

أيقن سو فنغ أنه سيموت، فبرد قلبه وكاد يتمنى لو أنه لم ينتقل إلى هذا العالم أصلًا

\n

“يا للكارثة!”

\n

“هل سأموت حقًا… ميتة بائسة بين أنياب وحش شرس؟”

\n

جف فم سو فنغ وامتلأت عيناه بالحزن، “لن أقبل بهذا!”

\n

وفي تلك اللحظة!

\n

“رنين! تم اكتشاف اندماج مثالي بين روح الهدف وجسده، وبدأ ربط نظام الزراعة الروحية بنقرة واحدة!”

\n

“1%… 12%… 56%…”

\n

ومع صوت لطيف، ظهرت أمام عيني سو فنغ لوحة شفافة، وعليها شريط تقدم أزرق فاتح يتحرك بسرعة!

\n

“ماذا!؟”

\n

“النظام—!!”

\n

أصيب سو فنغ بالذهول فورًا، وعجز تمامًا عن الكلام!

\n

لقد قرأ الكثير من الروايات، ولذلك كان يعرف بالطبع ما يعنيه ظهور النظام!

\n

فور حصول المرء على النظام، يمكن حتى لخنزير أن يحقق نجاحًا خارقًا، ويبلغ قمة الحياة، ويسيطر على العالم بأسره!

\n" + } +} \ No newline at end of file diff --git a/tests/fixtures/2442/chapters.json b/tests/fixtures/2442/chapters.json new file mode 100644 index 000000000..ada0124c9 --- /dev/null +++ b/tests/fixtures/2442/chapters.json @@ -0,0 +1,77 @@ +{ + "novel_id": 219910, + "total": 488, + "trimmed_original_total": 488, + "chapters": [ + { + "id": 221415, + "position": 1, + "number": "1", + "label": "الفصل 1", + "title": "هل يجب أن تكوني بهذه القسوة؟", + "url": "https://galaxynovels.com/novel/i-can-practice-the-god-level-system-with-one-click/chapter-1/%d8%a7%d9%84%d9%81%d8%b5%d9%84-1-%d9%87%d9%84-%d9%8a%d8%ac%d8%a8-%d8%a3%d9%86-%d8%aa%d9%83%d9%88%d9%86%d9%8a-%d8%a8%d9%87%d8%b0%d9%87-%d8%a7%d9%84%d9%82%d8%b3%d9%88%d8%a9%d8%9f/", + "content_api": "/wp-json/wor-reader-app/v1/chapters/221415", + "date": "يوليو 24, 2026", + "date_iso": "2026-07-24T20:00:00+00:00", + "views": 0, + "comments": 0, + "search": "1 الفصل 1 الفصل 1: هل يجب أن تكوني بهذه القسوة؟" + }, + { + "id": 221416, + "position": 2, + "number": "2", + "label": "الفصل 2", + "title": "هذا بالتأكيد أروع نظام!", + "url": "https://galaxynovels.com/novel/i-can-practice-the-god-level-system-with-one-click/chapter-2/%d8%a7%d9%84%d9%81%d8%b5%d9%84-2-%d9%87%d8%b0%d8%a7-%d8%a8%d8%a7%d9%84%d8%aa%d8%a3%d9%83%d9%8a%d8%af-%d8%a3%d8%b1%d9%88%d8%b9-%d9%86%d8%b8%d8%a7%d9%85/", + "content_api": "/wp-json/wor-reader-app/v1/chapters/221416", + "date": "يوليو 24, 2026", + "date_iso": "2026-07-24T20:00:00+00:00", + "views": 0, + "comments": 0, + "search": "2 الفصل 2 الفصل 2: هذا بالتأكيد أروع نظام!" + }, + { + "id": 221417, + "position": 3, + "number": "3", + "label": "الفصل 3", + "title": "تفعيل وظيفة الزراعة الروحية بنقرة واحدة!", + "url": "https://galaxynovels.com/novel/i-can-practice-the-god-level-system-with-one-click/chapter-3/%d8%a7%d9%84%d9%81%d8%b5%d9%84-3-%d8%aa%d9%81%d8%b9%d9%8a%d9%84-%d9%88%d8%b8%d9%8a%d9%81%d8%a9-%d8%a7%d9%84%d8%b2%d8%b1%d8%a7%d8%b9%d8%a9-%d8%a7%d9%84%d8%b1%d9%88%d8%ad%d9%8a%d8%a9-%d8%a8%d9%86%d9%82/", + "content_api": "/wp-json/wor-reader-app/v1/chapters/221417", + "date": "يوليو 24, 2026", + "date_iso": "2026-07-24T20:00:00+00:00", + "views": 0, + "comments": 0, + "search": "3 الفصل 3 الفصل 3: تفعيل وظيفة الزراعة الروحية بنقرة واحدة!" + }, + { + "id": 221418, + "position": 4, + "number": "4", + "label": "الفصل 4", + "title": "سأقتلك أيها الوغد!", + "url": "https://galaxynovels.com/novel/i-can-practice-the-god-level-system-with-one-click/chapter-4/%d8%a7%d9%84%d9%81%d8%b5%d9%84-4-%d8%b3%d8%a3%d9%82%d8%aa%d9%84%d9%83-%d8%a3%d9%8a%d9%87%d8%a7-%d8%a7%d9%84%d9%88%d8%ba%d8%af/", + "content_api": "/wp-json/wor-reader-app/v1/chapters/221418", + "date": "يوليو 24, 2026", + "date_iso": "2026-07-24T20:00:00+00:00", + "views": 0, + "comments": 0, + "search": "4 الفصل 4 الفصل 4: سأقتلك أيها الوغد!" + }, + { + "id": 221419, + "position": 5, + "number": "5", + "label": "الفصل 5", + "title": "أتظن أن الأمر انتهى؟ كلا!", + "url": "https://galaxynovels.com/novel/i-can-practice-the-god-level-system-with-one-click/chapter-5/%d8%a7%d9%84%d9%81%d8%b5%d9%84-5-%d8%a3%d8%aa%d8%b8%d9%86-%d8%a3%d9%86-%d8%a7%d9%84%d8%a3%d9%85%d8%b1-%d8%a7%d9%86%d8%aa%d9%87%d9%89%d8%9f-%d9%83%d9%84%d8%a7/", + "content_api": "/wp-json/wor-reader-app/v1/chapters/221419", + "date": "يوليو 24, 2026", + "date_iso": "2026-07-24T20:00:00+00:00", + "views": 0, + "comments": 0, + "search": "5 الفصل 5 الفصل 5: أتظن أن الأمر انتهى؟ كلا!" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/2442/novel.html b/tests/fixtures/2442/novel.html new file mode 100644 index 000000000..ab37d27b7 --- /dev/null +++ b/tests/fixtures/2442/novel.html @@ -0,0 +1,206 @@ + يمكنني ممارسة الزراعة الروحية بالنظام الاعظم بنقرة واحدة – قراءة الفصول المترجمة بجودة عالية + + +تجاوز إلى المحتوى
+ +
+
+🇨🇳
+مستمرة

يمكنني ممارسة الزراعة الروحية بالنظام الاعظم بنقرة واحدة

مؤلف الرواية: Miao Di

+ +الحالة +مستمرة + + + +دولة الرواية +رواية صينية +
+
+ +67٬740 +المشاهدات
+ +488 +عدد الفصول

عن الرواية

+ +
+ + + +الاشتراك +
+
+476

/chapter-\d+/.test(c.path)), + `got ${novel?.chapters?.length} chapters: ${JSON.stringify(novel?.chapters?.map(c => c.path))}`, + ); +} + +// --- 2. parseChapter: content served from the API (the only viable path) --- +let content = null; +try { + content = await plugin.parseChapter( + novel?.chapters?.[0]?.path || CHAPTER_PATH, + ); +} catch (err) { + console.log( + `FAIL C-1 parseChapter content >= 200, starts with

الفصل 1 -- parseChapter threw: ${err.message}`, + ); + failures++; +} +if (content !== null) { + const len = content.trim().length; + check( + 'C-1 parseChapter content >= 200, starts with

الفصل 1', + len >= 200 && content.startsWith('

الفصل 1'), + `got ${len} chars, starts ${JSON.stringify(content.slice(0, 20))}`, + ); +} + +// --- 3. Symptom pin: a 403 (regardless of cause) must reject with the exact +// user-facing error, never resolve silently. C-2 forces the 403 mode. --- +await expectReject( + 'C-2 403 rejects with /Could not reach site (403)/', + (() => { + denyAll = true; + return plugin.parseChapter(CHAPTER_PATH).finally(() => { + denyAll = false; + }); + })(), + /Could not reach site \(403\)/, +); + +// --- 4. Mechanism pin: the chapter API was used for the success case --- +check( + 'C-3 chapter API /wp-json/wor-reader-app/v1/chapters/ was fetched', + calls.some(c => c.url.includes('/wp-json/wor-reader-app/v1/chapters/')), + 'no API call in the fetch log', +); + +// --- 5. REQ-1 (last so it covers the whole run incl. C-2): every fetch init +// carried a User-Agent header --- +check( + 'REQ-1 every fetch init carries a User-Agent header', + calls.length > 0 && + calls.every( + c => + typeof c.headers['User-Agent'] === 'string' && + c.headers['User-Agent'].length > 0, + ), + `${calls.filter(c => !c.headers['User-Agent']).length} of ${calls.length} requests UA-less: ${JSON.stringify(calls.filter(c => !c.headers['User-Agent']).map(c => c.url))}`, +); + +console.log( + `\n${failures === 0 ? 'ALL GREEN' : failures + ' FAILING (RED - bug reproduced)'}`, +); +process.exit(failures === 0 ? 0 : 1);