From 197165ab0650dbed129d84a97cc223f79a3b7462 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 14:44:15 +0800 Subject: [PATCH 01/21] fix(linovelib): Searching not work --- plugins/chinese/linovelib.ts | 123 ++++----- plugins/index.ts | 490 ++++++++++++++++------------------- proxy.ts | 1 + 3 files changed, 285 insertions(+), 329 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index e9285cb1b..9f073cdb2 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -438,86 +438,75 @@ class Linovelib implements Plugin.PluginBase { searchTerm: string, pageNo: number, ): Promise { - const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`; + const url = `${this.site}/search.html?searchkey=${encodeURIComponent(searchTerm)}`; + + const body = await fetchText(url, { + headers: { + 'Referer': url, + 'Cookie': 'night=0', + }, + }); - const body = await fetchText(url); if (body === '') throw Error('无法获取搜索结果,请检查网络'); const pageCheerio = parseHTML(body); const novels: Plugin.NovelItem[] = []; - // const addPage = async (pageCheerio: CheerioAPI, redirect: string) => { - // const loadSearchResults = () => { - // pageCheerio(".book-ol .book-layout").each((i, el) => { - // let nUrl = pageCheerio(el).attr("href"); - - // const novelName = pageCheerio(el) - // .find(".book-title") - // .text(); - // const novelCover = pageCheerio(el) - // .find("div.book-cover > img") - // .attr("data-src"); - // const novelUrl = this.site + nUrl; - - // if (!nUrl) return; - - // novels.push({ - // name: novelName, - // url: novelUrl, - // cover: novelCover, - // }); - // }); - // }; - - // const novelResults = pageCheerio(".book-ol a.book-layout"); - // if (novelResults.length === 0) { - // } else { - // loadSearchResults(); - // } - - // if (redirect.length) { - // novels.length = 0; - // const novelName = pageCheerio( - // "#bookDetailWrapper .book-title" - // ).text(); - - // const novelCover = pageCheerio( - // "#bookDetailWrapper div.book-cover > img" - // ).attr("src"); - // const novelUrl = - // this.site + - // pageCheerio("#btnReadBook").attr("href")?.slice(0, -8) + - // ".html"; - // novels.push({ - // name: novelName, - // url: novelUrl, - // cover: novelCover, - // }); - // } - // }; + const loadSearchResults = () => { + pageCheerio('.book-ol .book-layout').each((i, el) => { + const nUrl = pageCheerio(el).attr('href')?.replace(this.site, ''); - // NOTE: don't know redirect is for what, comment out for now + const novelName = pageCheerio(el).find('.book-title').text(); + const novelCover = pageCheerio(el) + .find('div.book-cover > img') + .attr('data-src'); + const novelUrl = this.site + nUrl; - // const redirect = pageCheerio("div.book-layout").text(); - // await addPage(pageCheerio, redirect); + if (!nUrl) return; - pageCheerio('.book-ol .book-layout').each((i, el) => { - const nUrl = pageCheerio(el).attr('href'); + novels.push({ + name: novelName, + path: novelUrl, + cover: novelCover, + }); + }); + }; - const novelName = pageCheerio(el).find('.book-title').text(); - const novelCover = pageCheerio(el) - .find('div.book-cover > img') - .attr('data-src'); + const addPage = async (pageCheerio: CheerioAPI, redirect: string) => { + const novelResults = pageCheerio('.book-ol a.book-layout'); + if (novelResults.length === 0) { + // No results found, nothing to do + } else { + loadSearchResults(); + } - if (!nUrl) return; + if (redirect.length) { + novels.length = 0; + const novelName = pageCheerio('#bookDetailWrapper .book-title').text(); + + const novelCover = pageCheerio( + '#bookDetailWrapper div.book-cover > img', + ).attr('src'); + const novelUrl = + pageCheerio('#btnReadBook').attr('href')?.slice(0, -8) + '.html'; + novels.push({ + name: novelName, + path: novelUrl, + cover: novelCover, + }); + } + }; - novels.push({ - name: novelName, - path: nUrl, - cover: novelCover, - }); - }); + // NOTE: don't know redirect is for what, comment out for now + // Note: Found that Linovelib will redirect to the novel page if there's only one result,so uncommenting this out + + const redirect = pageCheerio('div.book-layout').text(); + if (redirect.length > 0) { + await addPage(pageCheerio, redirect); + } else { + loadSearchResults(); + } return novels; } diff --git a/plugins/index.ts b/plugins/index.ts index c98be3157..a765c7e1b 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -1,255 +1,238 @@ import { Plugin } from '@/types/plugin'; import p_0 from '@plugins/arabic/ArNovel[madara]'; import p_1 from '@plugins/arabic/Azora[madara]'; -import p_2 from '@plugins/arabic/FreeKolNovel[lightnovelwp]'; -import p_3 from '@plugins/arabic/HizoManga[madara]'; -import p_4 from '@plugins/arabic/KolNovel[lightnovelwp]'; -import p_5 from '@plugins/arabic/Novel4Up[madara]'; -import p_6 from '@plugins/arabic/NovelsParadise[lightnovelwp]'; -import p_7 from '@plugins/arabic/Olaoecyou[madara]'; -import p_8 from '@plugins/arabic/Riwyat[madara]'; -import p_9 from '@plugins/arabic/dilartube'; -import p_10 from '@plugins/arabic/rewayatclub'; +import p_2 from '@plugins/arabic/dilartube'; +import p_3 from '@plugins/arabic/FreeKolNovel[lightnovelwp]'; +import p_4 from '@plugins/arabic/HizoManga[madara]'; +import p_5 from '@plugins/arabic/KolNovel[lightnovelwp]'; +import p_6 from '@plugins/arabic/Novel4Up[madara]'; +import p_7 from '@plugins/arabic/NovelsParadise[lightnovelwp]'; +import p_8 from '@plugins/arabic/Olaoecyou[madara]'; +import p_9 from '@plugins/arabic/rewayatclub'; +import p_10 from '@plugins/arabic/Riwyat[madara]'; import p_11 from '@plugins/arabic/sunovels'; import p_12 from '@plugins/chinese/69shu'; -import p_13 from '@plugins/chinese/Quanben'; -import p_14 from '@plugins/chinese/ixdzs8'; -import p_15 from '@plugins/chinese/linovel'; -import p_16 from '@plugins/chinese/linovelib'; -import p_17 from '@plugins/chinese/linovelib_tw'; -import p_18 from '@plugins/chinese/novel543'; +import p_13 from '@plugins/chinese/ixdzs8'; +import p_14 from '@plugins/chinese/linovel'; +import p_15 from '@plugins/chinese/linovelib'; +import p_16 from '@plugins/chinese/linovelib_tw'; +import p_17 from '@plugins/chinese/novel543'; +import p_18 from '@plugins/chinese/Quanben'; import p_19 from '@plugins/english/AllNovelFull[readnovelfull]'; import p_20 from '@plugins/english/AllNovel[readnovelfull]'; -import p_21 from '@plugins/english/ArcaneTranslations[lightnovelwp]'; -import p_22 from '@plugins/english/BelleReservoir[madara]'; -import p_23 from '@plugins/english/BoxNovel[readnovelfull]'; -import p_24 from '@plugins/english/CPUnovel[lightnovelwp]'; -import p_25 from '@plugins/english/CitrusAurora[madara]'; -import p_26 from '@plugins/english/CoralBoutique[madara]'; -import p_27 from '@plugins/english/DaoNovel[madara]'; -import p_28 from '@plugins/english/DaoTranslate[lightnovelwp]'; -import p_29 from '@plugins/english/DaoistQuest[fictioneer]'; -import p_30 from '@plugins/english/DearestRosalie[fictioneer]'; -import p_31 from '@plugins/english/DragonTea[madara]'; -import p_32 from '@plugins/english/Dragonholic[madara]'; -import p_33 from '@plugins/english/DuskBlossoms[madara]'; -import p_34 from '@plugins/english/ElloTL[lightnovelwp]'; -import p_35 from '@plugins/english/Eternalune[madara]'; -import p_36 from '@plugins/english/EtudeTranslations[madara]'; -import p_37 from '@plugins/english/FanNovel[readwn]'; -import p_38 from '@plugins/english/FansMTL[readwn]'; -import p_39 from '@plugins/english/FansTranslations[madara]'; -import p_40 from '@plugins/english/FirstKissNovel[madara]'; -import p_41 from '@plugins/english/Foxaholic[madara]'; -import p_42 from '@plugins/english/FreeWebNovel[readnovelfull]'; -import p_43 from '@plugins/english/GalaxyTranslations[madara]'; -import p_44 from '@plugins/english/Guavaread[madara]'; -import p_45 from '@plugins/english/HiraethTranslation[madara]'; -import p_46 from '@plugins/english/HotNovelPub[hotnovelpub]'; -import p_47 from '@plugins/english/Ippotranslations[lightnovelwp]'; -import p_48 from '@plugins/english/KeopiTranslations[lightnovelwp]'; -import p_49 from '@plugins/english/KnoxT[lightnovelwp]'; -import p_50 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; -import p_51 from '@plugins/english/LibRead[readnovelfull]'; -import p_52 from '@plugins/english/LightNovelCave[lightnovelworld]'; -import p_53 from '@plugins/english/LightNovelHeaven[madara]'; -import p_54 from '@plugins/english/LightNovelPlus[readnovelfull]'; -import p_55 from '@plugins/english/LightNovelPubVip[lightnovelworld]'; -import p_56 from '@plugins/english/LightNovelUpdates[madara]'; -import p_57 from '@plugins/english/LightNovelWord[lightnovelworld]'; -import p_58 from '@plugins/english/LilyontheValley[fictioneer]'; -import p_59 from '@plugins/english/Ltnovel[readwn]'; -import p_60 from '@plugins/english/LunarLetters[madara]'; -import p_61 from '@plugins/english/MTLNovel[madara]'; -import p_62 from '@plugins/english/MTLNovel[mtlnovel]'; -import p_63 from '@plugins/english/Meownovel[madara]'; -import p_64 from '@plugins/english/MoonlightNovels[lightnovelwp]'; -import p_65 from '@plugins/english/MostNovel[madara]'; -import p_66 from '@plugins/english/MysticalSeries[madara]'; -import p_67 from '@plugins/english/NeoSekaiTranslations[madara]'; -import p_68 from '@plugins/english/NitroManga[madara]'; -import p_69 from '@plugins/english/NobleMTL[lightnovelwp]'; -import p_70 from '@plugins/english/NoiceTranslations[madara]'; -import p_71 from '@plugins/english/NovelBin[readnovelfull]'; -import p_72 from '@plugins/english/NovelCool[novelcool]'; -import p_73 from '@plugins/english/NovelFull[readnovelfull]'; -import p_74 from '@plugins/english/NovelLib[fictioneer]'; -import p_75 from '@plugins/english/NovelMultiverse[madara]'; -import p_76 from '@plugins/english/NovelOnline'; -import p_77 from '@plugins/english/NovelTranslate[madara]'; -import p_78 from '@plugins/english/NovelsKnight[lightnovelwp]'; -import p_79 from '@plugins/english/PandaMachineTranslations[lightnovelwp]'; -import p_80 from '@plugins/english/PastelTales[madara]'; -import p_81 from '@plugins/english/PenguinSquad[fictioneer]'; -import p_82 from '@plugins/english/Prizma[fictioneer]'; -import p_83 from '@plugins/english/Ranobes[ranobes]'; -import p_84 from '@plugins/english/Ranovel[madara]'; -import p_85 from '@plugins/english/ReadFanfic[madara]'; -import p_86 from '@plugins/english/ReadNovelFull[readnovelfull]'; -import p_87 from '@plugins/english/RequiemTranslations[lightnovelwp]'; -import p_88 from '@plugins/english/SalmonLatte[madara]'; -import p_89 from '@plugins/english/SleepyTranslations[madara]'; -import p_90 from '@plugins/english/SonicMTL[madara]'; -import p_91 from '@plugins/english/SrankManga[madara]'; -import p_92 from '@plugins/english/StorySeedling'; -import p_93 from '@plugins/english/SweetEscape[madara]'; -import p_94 from '@plugins/english/SystemTranslation[lightnovelwp]'; -import p_95 from '@plugins/english/TranslatinOtaku[madara]'; -import p_96 from '@plugins/english/TranslationWeaver[lightnovelwp]'; -import p_97 from '@plugins/english/UniversalNovel[lightnovelwp]'; -import p_98 from '@plugins/english/VandyTranslate[lightnovelwp]'; -import p_99 from '@plugins/english/VioletLily[madara]'; -import p_100 from '@plugins/english/WebNovelLover[madara]'; -import p_101 from '@plugins/english/WebNovelPub[lightnovelworld]'; -import p_102 from '@plugins/english/WebNovelTranslation[madara]'; -import p_103 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]'; -import p_104 from '@plugins/english/WooksTeahouse[madara]'; -import p_105 from '@plugins/english/WordExcerpt[madara]'; -import p_106 from '@plugins/english/WuxiaSpace[readwn]'; -import p_107 from '@plugins/english/WuxiaV[readwn]'; -import p_108 from '@plugins/english/WuxiaWorldSite[madara]'; -import p_109 from '@plugins/english/Wuxiabox[readwn]'; -import p_110 from '@plugins/english/Wuxiafox[readwn]'; -import p_111 from '@plugins/english/ZetroTranslation[madara]'; -import p_112 from '@plugins/english/ao3'; -import p_113 from '@plugins/english/bestlightnovel'; -import p_114 from '@plugins/english/chrysanthemumgarden'; -import p_115 from '@plugins/english/crimsonscrolls'; -import p_116 from '@plugins/english/divinedaolibrary'; -import p_117 from '@plugins/english/dreambigtl'; -import p_118 from '@plugins/english/earlynovel'; -import p_119 from '@plugins/english/faqwikius'; -import p_120 from '@plugins/english/fenrirrealm'; -import p_121 from '@plugins/english/fictionzone'; -import p_122 from '@plugins/english/foxteller'; -import p_123 from '@plugins/english/genesis'; -import p_124 from '@plugins/english/inkitt'; -import p_125 from '@plugins/english/kdtnovels'; -import p_126 from '@plugins/english/leafstudio'; -import p_127 from '@plugins/english/lightnovelpub'; -import p_128 from '@plugins/english/lightnoveltranslation'; -import p_129 from '@plugins/english/lnmtl'; -import p_130 from '@plugins/english/mtlreader'; -import p_131 from '@plugins/english/mvlempyr'; -import p_132 from '@plugins/english/novelbuddy'; -import p_133 from '@plugins/english/novelfire.paged'; -import p_134 from '@plugins/english/novelfire'; -import p_135 from '@plugins/english/novelhall'; -import p_136 from '@plugins/english/novelight'; -import p_137 from '@plugins/english/novelupdates'; -import p_138 from '@plugins/english/pawread'; -import p_139 from '@plugins/english/rainofsnow'; -import p_140 from '@plugins/english/readfrom'; -import p_141 from '@plugins/english/readlitenovel'; -import p_142 from '@plugins/english/reaperscans'; -import p_143 from '@plugins/english/relibrary'; -import p_144 from '@plugins/english/royalroad'; -import p_145 from '@plugins/english/scribblehub'; -import p_146 from '@plugins/english/vynovel'; -import p_147 from '@plugins/english/webnovel'; -import p_148 from '@plugins/english/wtrlab'; -import p_149 from '@plugins/english/wuxiaworld'; -import p_150 from '@plugins/french/LighNovelFR[lightnovelwp]'; -import p_151 from '@plugins/french/MTLNovel(FR)[mtlnovel]'; -import p_152 from '@plugins/french/MassNovel[madara]'; -import p_153 from '@plugins/french/WorldNovel[madara]'; -import p_154 from '@plugins/french/chireads'; -import p_155 from '@plugins/french/harkeneliwood'; -import p_156 from '@plugins/french/kisswood'; -import p_157 from '@plugins/french/noveldeglace'; -import p_158 from '@plugins/french/novhell'; -import p_159 from '@plugins/french/phenixscans'; -import p_160 from '@plugins/french/warriorlegendtrad'; -import p_161 from '@plugins/french/wuxialnscantrad'; -import p_162 from '@plugins/french/xiaowaz'; -import p_163 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]'; -import p_164 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]'; -import p_165 from '@plugins/indonesian/MeioNovel[madara]'; -import p_166 from '@plugins/indonesian/NovelBookID[madara]'; -import p_167 from '@plugins/indonesian/Risenovel[madara]'; -import p_168 from '@plugins/indonesian/SekteNovel[lightnovelwp]'; -import p_169 from '@plugins/indonesian/Vanovel[madara]'; -import p_170 from '@plugins/indonesian/WBNovel[madara]'; -import p_171 from '@plugins/indonesian/indowebnovel'; -import p_172 from '@plugins/indonesian/novelringan'; -import p_173 from '@plugins/indonesian/sakuranovel'; -import p_174 from '@plugins/japanese/Syosetu'; -import p_175 from '@plugins/japanese/kakuyomu'; -import p_176 from '@plugins/korean/Agitoon'; -import p_177 from '@plugins/korean/FortuneEternal[madara]'; -import p_178 from '@plugins/multi/komga'; -import p_179 from '@plugins/polish/novelki'; -import p_180 from '@plugins/portuguese/BetterNovels[lightnovelwp]'; -import p_181 from '@plugins/portuguese/CentralNovel[lightnovelwp]'; -import p_182 from '@plugins/portuguese/Kiniga[madara]'; -import p_183 from '@plugins/portuguese/LaNovels[hotnovelpub]'; -import p_184 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]'; -import p_185 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]'; -import p_186 from '@plugins/portuguese/blogdoamonnovels'; -import p_187 from '@plugins/portuguese/novelmania'; -import p_188 from '@plugins/portuguese/tsundoku'; -import p_189 from '@plugins/russian/Bookhamster[ifreedom]'; -import p_190 from '@plugins/russian/Erolate[rulate]'; -import p_191 from '@plugins/russian/EzNovels[hotnovelpub]'; -import p_192 from '@plugins/russian/LitSpace'; -import p_193 from '@plugins/russian/MTLNovel(RU)[mtlnovel]'; -import p_194 from '@plugins/russian/NovelCool(RU)[novelcool]'; -import p_195 from '@plugins/russian/Ranobes(RU)[ranobes]'; -import p_196 from '@plugins/russian/Rulate[rulate]'; -import p_197 from '@plugins/russian/authortoday'; -import p_198 from '@plugins/russian/bookriver'; -import p_199 from '@plugins/russian/ficbook'; -import p_200 from '@plugins/russian/jaomix'; -import p_201 from '@plugins/russian/neobook'; -import p_202 from '@plugins/russian/novelOvh'; -import p_203 from '@plugins/russian/novelTL'; -import p_204 from '@plugins/russian/ranobehub'; -import p_205 from '@plugins/russian/ranobelib'; -import p_206 from '@plugins/russian/ranoberf'; -import p_207 from '@plugins/russian/renovels'; -import p_208 from '@plugins/russian/ruvers'; -import p_209 from '@plugins/russian/topliba'; -import p_210 from '@plugins/russian/zelluloza'; -import p_211 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]'; -import p_212 from '@plugins/spanish/AllNovelRead[lightnovelwp]'; -import p_213 from '@plugins/spanish/AnimesHoy12[madara]'; -import p_214 from '@plugins/spanish/LightNovelDaily[hotnovelpub]'; -import p_215 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]'; -import p_216 from '@plugins/spanish/PanchoTranslations[madara]'; -import p_217 from '@plugins/spanish/TC&Sega[lightnovelwp]'; -import p_218 from '@plugins/spanish/TraduccionesAmistosas[madara]'; -import p_219 from '@plugins/spanish/hasutl'; -import p_220 from '@plugins/spanish/novelasligera'; -import p_221 from '@plugins/spanish/novelawuxia'; -import p_222 from '@plugins/spanish/oasistranslations'; -import p_223 from '@plugins/spanish/skynovels'; -import p_224 from '@plugins/spanish/tunovelaligera'; -import p_225 from '@plugins/spanish/yukitls'; -import p_226 from '@plugins/thai/NovelLucky[madara]'; -import p_227 from '@plugins/thai/NovelPDF[madara]'; -import p_228 from '@plugins/turkish/ArazNovel[madara]'; -import p_229 from '@plugins/turkish/EKTAPLAR[madara]'; -import p_230 from '@plugins/turkish/KodeksLibrary[lightnovelwp]'; -import p_231 from '@plugins/turkish/MangaTR'; -import p_232 from '@plugins/turkish/NABSCANS[madara]'; -import p_233 from '@plugins/turkish/Namevt[lightnovelwp]'; -import p_234 from '@plugins/turkish/NovelTR[lightnovelwp]'; -import p_235 from '@plugins/turkish/Noveloku[madara]'; -import p_236 from '@plugins/turkish/RagnarScans[madara]'; -import p_237 from '@plugins/turkish/ThNovels[hotnovelpub]'; -import p_238 from '@plugins/turkish/TurkceLightNovels[madara]'; -import p_239 from '@plugins/turkish/WebNovelOku[madara]'; -import p_240 from '@plugins/turkish/epiknovel'; -import p_241 from '@plugins/turkish/kakikata[madara]'; -import p_242 from '@plugins/ukrainian/bakainua'; -import p_243 from '@plugins/ukrainian/smakolykytl'; -import p_244 from '@plugins/ukrainian/uaranobeclub'; -import p_245 from '@plugins/vietnamese/LNHako'; -import p_246 from '@plugins/vietnamese/Truyenconect'; -import p_247 from '@plugins/vietnamese/lightnovelvn'; -import p_248 from '@plugins/vietnamese/nettruyen'; -import p_249 from '@plugins/vietnamese/truyenchu'; -import p_250 from '@plugins/vietnamese/truyenfull'; +import p_21 from '@plugins/english/ao3'; +import p_22 from '@plugins/english/ArcaneTranslations[lightnovelwp]'; +import p_23 from '@plugins/english/BelleReservoir[madara]'; +import p_24 from '@plugins/english/BoxNovel[readnovelfull]'; +import p_25 from '@plugins/english/chrysanthemumgarden'; +import p_26 from '@plugins/english/CitrusAurora[madara]'; +import p_27 from '@plugins/english/CoralBoutique[madara]'; +import p_28 from '@plugins/english/CPUnovel[lightnovelwp]'; +import p_29 from '@plugins/english/crimsonscrolls'; +import p_30 from '@plugins/english/DaoistQuest[fictioneer]'; +import p_31 from '@plugins/english/DaoNovel[madara]'; +import p_32 from '@plugins/english/DaoTranslate[lightnovelwp]'; +import p_33 from '@plugins/english/DearestRosalie[fictioneer]'; +import p_34 from '@plugins/english/divinedaolibrary'; +import p_35 from '@plugins/english/Dragonholic[madara]'; +import p_36 from '@plugins/english/DragonTea[madara]'; +import p_37 from '@plugins/english/dreambigtl'; +import p_38 from '@plugins/english/DuskBlossoms[madara]'; +import p_39 from '@plugins/english/ElloTL[lightnovelwp]'; +import p_40 from '@plugins/english/Eternalune[madara]'; +import p_41 from '@plugins/english/EtudeTranslations[madara]'; +import p_42 from '@plugins/english/FanNovel[readwn]'; +import p_43 from '@plugins/english/FansMTL[readwn]'; +import p_44 from '@plugins/english/FansTranslations[madara]'; +import p_45 from '@plugins/english/faqwikius'; +import p_46 from '@plugins/english/fenrirrealm'; +import p_47 from '@plugins/english/fictionzone'; +import p_48 from '@plugins/english/FirstKissNovel[madara]'; +import p_49 from '@plugins/english/Foxaholic[madara]'; +import p_50 from '@plugins/english/foxteller'; +import p_51 from '@plugins/english/FreeWebNovel[readnovelfull]'; +import p_52 from '@plugins/english/GalaxyTranslations[madara]'; +import p_53 from '@plugins/english/genesis'; +import p_54 from '@plugins/english/Guavaread[madara]'; +import p_55 from '@plugins/english/HiraethTranslation[madara]'; +import p_56 from '@plugins/english/HotNovelPub[hotnovelpub]'; +import p_57 from '@plugins/english/inkitt'; +import p_58 from '@plugins/english/Ippotranslations[lightnovelwp]'; +import p_59 from '@plugins/english/kdtnovels'; +import p_60 from '@plugins/english/KeopiTranslations[lightnovelwp]'; +import p_61 from '@plugins/english/KnoxT[lightnovelwp]'; +import p_62 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; +import p_63 from '@plugins/english/leafstudio'; +import p_64 from '@plugins/english/LibRead[readnovelfull]'; +import p_65 from '@plugins/english/LightNovelCave[lightnovelworld]'; +import p_66 from '@plugins/english/LightNovelHeaven[madara]'; +import p_67 from '@plugins/english/LightNovelPlus[readnovelfull]'; +import p_68 from '@plugins/english/LightNovelPubVip[lightnovelworld]'; +import p_69 from '@plugins/english/lightnoveltranslation'; +import p_70 from '@plugins/english/LightNovelUpdates[madara]'; +import p_71 from '@plugins/english/LilyontheValley[fictioneer]'; +import p_72 from '@plugins/english/lnmtl'; +import p_73 from '@plugins/english/Ltnovel[readwn]'; +import p_74 from '@plugins/english/LunarLetters[madara]'; +import p_75 from '@plugins/english/Meownovel[madara]'; +import p_76 from '@plugins/english/MoonlightNovels[lightnovelwp]'; +import p_77 from '@plugins/english/MostNovel[madara]'; +import p_78 from '@plugins/english/MTLNovel[madara]'; +import p_79 from '@plugins/english/MTLNovel[mtlnovel]'; +import p_80 from '@plugins/english/mtlreader'; +import p_81 from '@plugins/english/mvlempyr'; +import p_82 from '@plugins/english/MysticalSeries[madara]'; +import p_83 from '@plugins/english/NeoSekaiTranslations[madara]'; +import p_84 from '@plugins/english/NitroManga[madara]'; +import p_85 from '@plugins/english/NobleMTL[lightnovelwp]'; +import p_86 from '@plugins/english/NoiceTranslations[madara]'; +import p_87 from '@plugins/english/NovelBin[readnovelfull]'; +import p_88 from '@plugins/english/novelbuddy'; +import p_89 from '@plugins/english/NovelCool[novelcool]'; +import p_90 from '@plugins/english/novelfire'; +import p_91 from '@plugins/english/NovelFull[readnovelfull]'; +import p_92 from '@plugins/english/novelhall'; +import p_93 from '@plugins/english/novelight'; +import p_94 from '@plugins/english/NovelLib[fictioneer]'; +import p_95 from '@plugins/english/NovelMultiverse[madara]'; +import p_96 from '@plugins/english/NovelOnline'; +import p_97 from '@plugins/english/NovelsKnight[lightnovelwp]'; +import p_98 from '@plugins/english/NovelTranslate[madara]'; +import p_99 from '@plugins/english/novelupdates'; +import p_100 from '@plugins/english/PandaMachineTranslations[lightnovelwp]'; +import p_101 from '@plugins/english/PastelTales[madara]'; +import p_102 from '@plugins/english/pawread'; +import p_103 from '@plugins/english/PenguinSquad[fictioneer]'; +import p_104 from '@plugins/english/Prizma[fictioneer]'; +import p_105 from '@plugins/english/rainofsnow'; +import p_106 from '@plugins/english/Ranobes[ranobes]'; +import p_107 from '@plugins/english/Ranovel[madara]'; +import p_108 from '@plugins/english/ReadFanfic[madara]'; +import p_109 from '@plugins/english/readfrom'; +import p_110 from '@plugins/english/ReadNovelFull[readnovelfull]'; +import p_111 from '@plugins/english/relibrary'; +import p_112 from '@plugins/english/RequiemTranslations[lightnovelwp]'; +import p_113 from '@plugins/english/royalroad'; +import p_114 from '@plugins/english/SalmonLatte[madara]'; +import p_115 from '@plugins/english/scribblehub'; +import p_116 from '@plugins/english/SleepyTranslations[madara]'; +import p_117 from '@plugins/english/SonicMTL[madara]'; +import p_118 from '@plugins/english/SrankManga[madara]'; +import p_119 from '@plugins/english/StorySeedling'; +import p_120 from '@plugins/english/SweetEscape[madara]'; +import p_121 from '@plugins/english/SystemTranslation[lightnovelwp]'; +import p_122 from '@plugins/english/TranslatinOtaku[madara]'; +import p_123 from '@plugins/english/TranslationWeaver[lightnovelwp]'; +import p_124 from '@plugins/english/UniversalNovel[lightnovelwp]'; +import p_125 from '@plugins/english/VandyTranslate[lightnovelwp]'; +import p_126 from '@plugins/english/VioletLily[madara]'; +import p_127 from '@plugins/english/vynovel'; +import p_128 from '@plugins/english/webnovel'; +import p_129 from '@plugins/english/WebNovelLover[madara]'; +import p_130 from '@plugins/english/WebNovelPub[lightnovelworld]'; +import p_131 from '@plugins/english/WebNovelTranslation[madara]'; +import p_132 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]'; +import p_133 from '@plugins/english/WooksTeahouse[madara]'; +import p_134 from '@plugins/english/WordExcerpt[madara]'; +import p_135 from '@plugins/english/wtrlab'; +import p_136 from '@plugins/english/Wuxiabox[readwn]'; +import p_137 from '@plugins/english/Wuxiafox[readwn]'; +import p_138 from '@plugins/english/WuxiaSpace[readwn]'; +import p_139 from '@plugins/english/WuxiaV[readwn]'; +import p_140 from '@plugins/english/wuxiaworld'; +import p_141 from '@plugins/english/WuxiaWorldSite[madara]'; +import p_142 from '@plugins/english/ZetroTranslation[madara]'; +import p_143 from '@plugins/french/chireads'; +import p_144 from '@plugins/french/harkeneliwood'; +import p_145 from '@plugins/french/kisswood'; +import p_146 from '@plugins/french/LighNovelFR[lightnovelwp]'; +import p_147 from '@plugins/french/MassNovel[madara]'; +import p_148 from '@plugins/french/MTLNovel(FR)[mtlnovel]'; +import p_149 from '@plugins/french/noveldeglace'; +import p_150 from '@plugins/french/novhell'; +import p_151 from '@plugins/french/warriorlegendtrad'; +import p_152 from '@plugins/french/WorldNovel[madara]'; +import p_153 from '@plugins/french/wuxialnscantrad'; +import p_154 from '@plugins/french/xiaowaz'; +import p_155 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]'; +import p_156 from '@plugins/indonesian/indowebnovel'; +import p_157 from '@plugins/indonesian/MeioNovel[madara]'; +import p_158 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]'; +import p_159 from '@plugins/indonesian/NovelBookID[madara]'; +import p_160 from '@plugins/indonesian/sakuranovel'; +import p_161 from '@plugins/indonesian/SekteNovel[lightnovelwp]'; +import p_162 from '@plugins/indonesian/Vanovel[madara]'; +import p_163 from '@plugins/indonesian/WBNovel[madara]'; +import p_164 from '@plugins/japanese/kakuyomu'; +import p_165 from '@plugins/japanese/Syosetu'; +import p_166 from '@plugins/korean/Agitoon'; +import p_167 from '@plugins/korean/FortuneEternal[madara]'; +import p_168 from '@plugins/multi/komga'; +import p_169 from '@plugins/polish/novelki'; +import p_170 from '@plugins/portuguese/BetterNovels[lightnovelwp]'; +import p_171 from '@plugins/portuguese/blogdoamonnovels'; +import p_172 from '@plugins/portuguese/CentralNovel[lightnovelwp]'; +import p_173 from '@plugins/portuguese/Kiniga[madara]'; +import p_174 from '@plugins/portuguese/LaNovels[hotnovelpub]'; +import p_175 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]'; +import p_176 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]'; +import p_177 from '@plugins/portuguese/novelmania'; +import p_178 from '@plugins/portuguese/tsundoku'; +import p_179 from '@plugins/russian/authortoday'; +import p_180 from '@plugins/russian/Bookhamster[ifreedom]'; +import p_181 from '@plugins/russian/bookriver'; +import p_182 from '@plugins/russian/Erolate[rulate]'; +import p_183 from '@plugins/russian/EzNovels[hotnovelpub]'; +import p_184 from '@plugins/russian/ficbook'; +import p_185 from '@plugins/russian/jaomix'; +import p_186 from '@plugins/russian/MTLNovel(RU)[mtlnovel]'; +import p_187 from '@plugins/russian/neobook'; +import p_188 from '@plugins/russian/NovelCool(RU)[novelcool]'; +import p_189 from '@plugins/russian/novelTL'; +import p_190 from '@plugins/russian/ranobehub'; +import p_191 from '@plugins/russian/ranobelib'; +import p_192 from '@plugins/russian/ranoberf'; +import p_193 from '@plugins/russian/Ranobes(RU)[ranobes]'; +import p_194 from '@plugins/russian/renovels'; +import p_195 from '@plugins/russian/Rulate[rulate]'; +import p_196 from '@plugins/russian/topliba'; +import p_197 from '@plugins/russian/zelluloza'; +import p_198 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]'; +import p_199 from '@plugins/spanish/AllNovelRead[lightnovelwp]'; +import p_200 from '@plugins/spanish/AnimesHoy12[madara]'; +import p_201 from '@plugins/spanish/hasutl'; +import p_202 from '@plugins/spanish/LightNovelDaily[hotnovelpub]'; +import p_203 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]'; +import p_204 from '@plugins/spanish/novelasligera'; +import p_205 from '@plugins/spanish/novelawuxia'; +import p_206 from '@plugins/spanish/oasistranslations'; +import p_207 from '@plugins/spanish/PanchoTranslations[madara]'; +import p_208 from '@plugins/spanish/skynovels'; +import p_209 from '@plugins/spanish/TC&Sega[lightnovelwp]'; +import p_210 from '@plugins/spanish/TraduccionesAmistosas[madara]'; +import p_211 from '@plugins/spanish/tunovelaligera'; +import p_212 from '@plugins/spanish/yukitls'; +import p_213 from '@plugins/thai/NovelLucky[madara]'; +import p_214 from '@plugins/thai/NovelPDF[madara]'; +import p_215 from '@plugins/turkish/ArazNovel[madara]'; +import p_216 from '@plugins/turkish/EKTAPLAR[madara]'; +import p_217 from '@plugins/turkish/epiknovel'; +import p_218 from '@plugins/turkish/kakikata[madara]'; +import p_219 from '@plugins/turkish/KodeksLibrary[lightnovelwp]'; +import p_220 from '@plugins/turkish/MangaTR'; +import p_221 from '@plugins/turkish/NABSCANS[madara]'; +import p_222 from '@plugins/turkish/Namevt[lightnovelwp]'; +import p_223 from '@plugins/turkish/Noveloku[madara]'; +import p_224 from '@plugins/turkish/NovelTR[lightnovelwp]'; +import p_225 from '@plugins/turkish/RagnarScans[madara]'; +import p_226 from '@plugins/turkish/ThNovels[hotnovelpub]'; +import p_227 from '@plugins/turkish/TurkceLightNovels[madara]'; +import p_228 from '@plugins/turkish/WebNovelOku[madara]'; +import p_229 from '@plugins/ukrainian/bakainua'; +import p_230 from '@plugins/ukrainian/smakolykytl'; +import p_231 from '@plugins/vietnamese/lightnovelvn'; +import p_232 from '@plugins/vietnamese/LNHako'; +import p_233 from '@plugins/vietnamese/nettruyen'; const PLUGINS: Plugin.PluginBase[] = [ p_0, @@ -486,22 +469,5 @@ const PLUGINS: Plugin.PluginBase[] = [ p_231, p_232, p_233, - p_234, - p_235, - p_236, - p_237, - p_238, - p_239, - p_240, - p_241, - p_242, - p_243, - p_244, - p_245, - p_246, - p_247, - p_248, - p_249, - p_250, ]; export default PLUGINS; diff --git a/proxy.ts b/proxy.ts index 5b99dfeb0..1a9adea66 100644 --- a/proxy.ts +++ b/proxy.ts @@ -66,6 +66,7 @@ const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { } res.setHeader('Access-Control-Allow-Origin', settings.CLIENT_HOST); res.setHeader('Access-Control-Allow-Credentials', 'true'); + req.headers.referer = rawUrl; if (req.method === 'OPTIONS') { res.statusCode = 200; res.end(); From f0399ac584a2d574594e073eb0e1bb2bec523cc4 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:05:38 +0800 Subject: [PATCH 02/21] ci: fix branch cannot be correctly identified in forked environment --- scripts/build-plugin-manifest.js | 3 ++- scripts/publish-plugins.sh | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/build-plugin-manifest.js b/scripts/build-plugin-manifest.js index d76c15a4c..cab5eec47 100644 --- a/scripts/build-plugin-manifest.js +++ b/scripts/build-plugin-manifest.js @@ -10,13 +10,14 @@ const REMOTE = execSync('git remote get-url origin') const CURRENT_BRANCH = execSync('git branch --show-current') .toString() .replace(/[\s\n]/g, ''); +const BRANCH = process.env.BRANCH ? process.env.BRANCH : CURRENT_BRANCH; const matched = REMOTE.match(/([^:/]+?)\/([^/.]+)(\.git)?$/); if (!matched) throw Error('Cant parse git url'); const USERNAME = matched[1]; const REPO = matched[2]; const USER_CONTENT_LINK = process.env.USER_CONTENT_BASE ? process.env.USER_CONTENT_BASE - : `https://raw.githubusercontent.com/${USERNAME}/${REPO}/${CURRENT_BRANCH}`; + : `https://raw.githubusercontent.com/${USERNAME}/${REPO}/${BRANCH}`; const STATIC_LINK = `${USER_CONTENT_LINK}/public/static`; // Use legacy .js/src/plugins path for backward compatibility diff --git a/scripts/publish-plugins.sh b/scripts/publish-plugins.sh index 897e366da..1c6b70ab9 100755 --- a/scripts/publish-plugins.sh +++ b/scripts/publish-plugins.sh @@ -7,6 +7,7 @@ fi current=`git rev-parse --abbrev-ref HEAD` version=`node -e "console.log(require('./package.json').version);"` dist="plugins/v$version" +export BRANCH = $dist echo "Publishing plugins: $current -> $dist (v$version)" From 0ec3f73e24b239fd3be53dccff3ea25c82badfc0 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:11:52 +0800 Subject: [PATCH 03/21] fix: BRANCH cannot be identified --- scripts/publish-plugins.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/publish-plugins.sh b/scripts/publish-plugins.sh index 1c6b70ab9..fba363c69 100755 --- a/scripts/publish-plugins.sh +++ b/scripts/publish-plugins.sh @@ -7,7 +7,6 @@ fi current=`git rev-parse --abbrev-ref HEAD` version=`node -e "console.log(require('./package.json').version);"` dist="plugins/v$version" -export BRANCH = $dist echo "Publishing plugins: $current -> $dist (v$version)" @@ -35,7 +34,7 @@ if [[ "$1" == "--all-branches" ]]; then echo "Compiling TypeScript..." npx tsc --project tsconfig.production.json echo "# $branch" >> $GITHUB_STEP_SUMMARY - npm run build:manifest -- --only-new 2>> $GITHUB_STEP_SUMMARY + BRANCH=$dist npm run build:manifest -- --only-new 2>> $GITHUB_STEP_SUMMARY if [ ! -d ".dist" ] || [ -z "$(ls -A .dist)" ]; then echo "❌ ERROR: Manifest generation failed - .dist is missing or empty" exit 1 From 514aca65df3710f37fc83fcd7b81f09c8c332f44 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:37:16 +0800 Subject: [PATCH 04/21] fix(linovelib): correctly get cover image in redirect case of searching --- plugins/chinese/linovelib.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 9f073cdb2..f8b43b9f4 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -34,7 +34,8 @@ class Linovelib implements Plugin.PluginBase { const novelName = loadedCheerio(el).find('.book-title').text(); const novelCover = loadedCheerio(el) .find('div.book-cover > img') - .attr('data-src'); + .attr('data-src') + ?.replace('/https', 'https'); if (!url) return; const novel = { @@ -483,10 +484,10 @@ class Linovelib implements Plugin.PluginBase { if (redirect.length) { novels.length = 0; - const novelName = pageCheerio('#bookDetailWrapper .book-title').text(); + const novelName = pageCheerio('.book-detail-info .book-title').text(); const novelCover = pageCheerio( - '#bookDetailWrapper div.book-cover > img', + '.book-detail-info div.module-item-cover > img', ).attr('src'); const novelUrl = pageCheerio('#btnReadBook').attr('href')?.slice(0, -8) + '.html'; From 4c2001294a55dfcf38fc8ee0b34a3a1a4de2a63e Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:37:54 +0800 Subject: [PATCH 05/21] fix: why we need a prefix slash here? remove it --- src/components/novel-card.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/novel-card.tsx b/src/components/novel-card.tsx index e82603563..89221157c 100644 --- a/src/components/novel-card.tsx +++ b/src/components/novel-card.tsx @@ -25,10 +25,7 @@ export function NovelCard({ novel, onParse }: NovelCardProps) {
{novel.name} From cf8061b1f8dbd4d9d31a46c9b06c7a1125f3d61b Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:39:27 +0800 Subject: [PATCH 06/21] chore(linovelib): update linovelib plugin version --- plugins/chinese/linovelib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index f8b43b9f4..dde3b7ce9 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -9,7 +9,7 @@ class Linovelib implements Plugin.PluginBase { name = 'Linovelib'; icon = 'src/cn/linovelib/icon.png'; site = 'https://www.bilinovel.com'; - version = '1.1.3'; + version = '1.1.4'; async popularNovels( pageNo: number, From ac17ed04d6902721558063680b89b7c9b48ad172 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 15:52:22 +0800 Subject: [PATCH 07/21] fix: before LNReader puts its default UA to chrome, forcely set LNReader plugin's UA to Chrome's UA --- plugins/chinese/linovelib.ts | 33 ++++++++++++++++++++++++++++++++- src/lib/fetch.ts | 2 +- src/libs/fetch.ts | 8 +++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index dde3b7ce9..13b6ac150 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -1,9 +1,40 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; -import { fetchText } from '@libs/fetch'; +import { FetchInit, fetchText as ft } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; +async function fetchText(url: string, init?: FetchInit) { + const actInit = (() => { + if (init?.headers) { + if (init.headers instanceof Headers) { + if (!init.headers.get('User-Agent')) { + init.headers.set( + 'User-Agent', + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + ); + } + } else { + init.headers = { + 'User-Agent': + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + ...init.headers, + }; + } + } else { + init = { + ...init, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + }, + }; + } + return init; + })(); + return await ft(url, actInit); +} + class Linovelib implements Plugin.PluginBase { id = 'linovelib'; name = 'Linovelib'; diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 401d2b69d..7331906c8 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -2,7 +2,7 @@ import { parse as parseProto } from 'protobufjs'; -type FetchInit = { +export type FetchInit = { headers?: Record | Headers; method?: string; body?: FormData | string; diff --git a/src/libs/fetch.ts b/src/libs/fetch.ts index 8d63d6008..e7cb13497 100644 --- a/src/libs/fetch.ts +++ b/src/libs/fetch.ts @@ -2,4 +2,10 @@ * Backward compatibility for 3.0.0 - Re-exports from new location * TODO: Remove in 4.0.0 */ -export { fetchApi, fetchText, fetchProto, fetchFile } from '../lib/fetch'; +export { + fetchApi, + fetchText, + fetchProto, + fetchFile, + FetchInit, +} from '../lib/fetch'; From 4f44ee847392bc713e9f855cc8db48e139c664e2 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 16:00:09 +0800 Subject: [PATCH 08/21] refactor: intgrates FetchInit into plugin --- plugins/chinese/linovelib.ts | 16 ++++++++++++++-- src/lib/fetch.ts | 2 +- src/libs/fetch.ts | 8 +------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 13b6ac150..2be0f46c3 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -1,9 +1,21 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; -import { FetchInit, fetchText as ft } from '@libs/fetch'; +import { fetchText as ft } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; +type FetchInit = { + headers?: Record | Headers; + method?: string; + body?: FormData | string; + [x: string]: + | string + | Record + | undefined + | FormData + | Headers; +}; + async function fetchText(url: string, init?: FetchInit) { const actInit = (() => { if (init?.headers) { @@ -493,7 +505,7 @@ class Linovelib implements Plugin.PluginBase { const novelCover = pageCheerio(el) .find('div.book-cover > img') .attr('data-src'); - const novelUrl = this.site + nUrl; + const novelUrl = nUrl || ''; if (!nUrl) return; diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts index 7331906c8..401d2b69d 100644 --- a/src/lib/fetch.ts +++ b/src/lib/fetch.ts @@ -2,7 +2,7 @@ import { parse as parseProto } from 'protobufjs'; -export type FetchInit = { +type FetchInit = { headers?: Record | Headers; method?: string; body?: FormData | string; diff --git a/src/libs/fetch.ts b/src/libs/fetch.ts index e7cb13497..8d63d6008 100644 --- a/src/libs/fetch.ts +++ b/src/libs/fetch.ts @@ -2,10 +2,4 @@ * Backward compatibility for 3.0.0 - Re-exports from new location * TODO: Remove in 4.0.0 */ -export { - fetchApi, - fetchText, - fetchProto, - fetchFile, - FetchInit, -} from '../lib/fetch'; +export { fetchApi, fetchText, fetchProto, fetchFile } from '../lib/fetch'; From 0d20a183e7c877f8df95d7d82eceae0d1f34319c Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 16:14:51 +0800 Subject: [PATCH 09/21] feat: add chapter name transform --- plugins/chinese/linovelib.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 2be0f46c3..94b79c481 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -160,10 +160,17 @@ class Linovelib implements Plugin.PluginBase { } const chapterUrl = `/novel/${novelId}/${chapterId}.html`; + const chapNameTransDict: Record = { + '\u004e': '\u5973', + }; const chapterName = volumeName + ' — ' + - chaptersLoadedCheerio(el).find('.chapter-index').text().trim(); + chaptersLoadedCheerio(el) + .find('.chapter-index') + .text() + .trim() + .replace(/./g, char => chapNameTransDict[char] || char); const releaseDate = null; if (!chapterId) return; From d4c2b00ec93878fab8375c43974a76d590a4e581 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Wed, 14 Jan 2026 23:40:51 +0800 Subject: [PATCH 10/21] feat(linovelib): add decryptor for shuffle scramble of original html --- package-lock.json | 6 +- package.json | 1 + plugins/chinese/linovelib.ts | 142 +++++++++++++++++++++++++++++------ 3 files changed, 124 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3443fa743..4e591b70a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,6 +48,7 @@ "@vitejs/plugin-react-swc": "^3.9.0", "cheerio": "^1.0.0", "dayjs": "^1.11.13", + "domhandler": "^5.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "globals": "^15.6.0", @@ -4755,9 +4756,10 @@ }, "node_modules/domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -12030,7 +12032,7 @@ }, "domhandler": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "requires": { diff --git a/package.json b/package.json index 1d30a90d6..74fc10e42 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@vitejs/plugin-react-swc": "^3.9.0", "cheerio": "^1.0.0", "dayjs": "^1.11.13", + "domhandler": "^5.0.3", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "globals": "^15.6.0", diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 94b79c481..969daa506 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -3,6 +3,7 @@ import { fetchText as ft } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; +import { type Element, type AnyNode } from 'domhandler'; type FetchInit = { headers?: Record | Headers; @@ -16,6 +17,119 @@ type FetchInit = { | Headers; }; +export class LinovelibDecrpytor { + public static decrypt($: CheerioAPI): string { + const chapterId = this.extractChapterId($); + const container = $('#acontent'); + if (!container.length) return ''; + + container.find('p').each((_, el) => { + const $el = $(el); + const innerHtml = $el.html(); + if (innerHtml) { + const cleanedHtml = innerHtml.replace(/^\s+|(?<=>)\s+/g, ''); + $el.html(cleanedHtml); + } + }); + + container.find('img.imagecontent').each((_, el) => { + const imgSrc = $(el).attr('data-src') || $(el).attr('src'); + if (imgSrc) { + $(el) + .attr('src', imgSrc) + .removeAttr('data-src') + .removeClass('lazyload'); + } + }); + + container.find('div.co').remove(); + + const allChildren = container + .contents() + .toArray() + .filter(node => !(node.type === 'tag' && node.tagName === 'div')); + + const sortableEntries: { element: Element; originalPos: number }[] = []; + + allChildren.forEach((node, index) => { + if (node.type === 'tag' && node.tagName === 'p') { + const text = $(node).text().trim(); + if (text.length > 0) { + sortableEntries.push({ element: node, originalPos: index }); + } + } + }); + + const pCount = sortableEntries.length; + if (pCount <= 20) { + return container.html() || ''; + } + + const seed = parseInt(chapterId, 10) * 127 + 235; + + const dynamicIndices = Array.from( + { length: pCount - 20 }, + (_, i) => i + 20, + ); + const shuffledIndices = this.shuffle(dynamicIndices, seed); + + const fullMapping = Array.from({ length: 20 }, (_, i) => i).concat( + shuffledIndices, + ); + + const restoredChildren: (AnyNode | null)[] = [...allChildren]; + + sortableEntries.forEach((entry, i) => { + const targetLogicalPos = fullMapping[i]; + const actualSlot = sortableEntries[targetLogicalPos]?.originalPos; + restoredChildren[actualSlot] = entry.element; + }); + + const newContainer = $('
'); + restoredChildren.forEach(node => { + if (node && node.type === 'tag') { + newContainer.append($(node)); + newContainer.append('\n'); + } + }); + + return newContainer.html() || ''; + } + + private static shuffle(array: number[], seed: number): number[] { + let currentSeed = seed; + const result = [...array]; + const len = result.length; + + for (let i = len - 1; i > 0; i--) { + currentSeed = (currentSeed * 9302 + 49397) % 233280; + const j = Math.floor((currentSeed / 233280) * (i + 1)); + + // 交换 + const temp = result[i]; + result[i] = result[j]; + result[j] = temp; + } + return result; + } + + private static extractChapterId($: CheerioAPI): string { + const scriptTags = $('script'); + let chapterId = ''; + scriptTags.each((_, el) => { + const scriptContent = $(el).html(); + if (scriptContent) { + const match = scriptContent.match(/chapterid\s*:\s*'(\d+)'/); + if (match && match[1]) { + chapterId = match[1]; + return false; // Break the loop + } + } + }); + return chapterId; + } +} + async function fetchText(url: string, init?: FetchInit) { const actInit = (() => { if (init?.headers) { @@ -52,7 +166,7 @@ class Linovelib implements Plugin.PluginBase { name = 'Linovelib'; icon = 'src/cn/linovelib/icon.png'; site = 'https://www.bilinovel.com'; - version = '1.1.4'; + version = '1.2.0'; async popularNovels( pageNo: number, @@ -415,28 +529,10 @@ class Linovelib implements Plugin.PluginBase { }; const addPage = async (pageCheerio: CheerioAPI) => { const formatPage = async () => { - // Remove JS and notice of the website - pageCheerio( - '#acontent .adsbygoogle, #acontent script, center', - ).remove(); - - // Load lazyloaded images - pageCheerio('#acontent img.imagecontent').each((i, el) => { - // Sometimes images are either in data-src or src - const imgSrc = - pageCheerio(el).attr('data-src') || pageCheerio(el).attr('src'); - if (imgSrc) { - // Clean up img element - pageCheerio(el) - .attr('src', imgSrc) - .removeAttr('data-src') - .removeClass('lazyload'); - } - }); - - // Recover the original character - pageText = pageCheerio('#acontent').html() || ''; - pageText = pageText.replace(/./g, char => skillgg[char] || char); + pageText = LinovelibDecrpytor.decrypt(pageCheerio).replace( + /./g, + char => skillgg[char] || char, + ); return Promise.resolve(); }; From 73d224b10d0429691ba92876b6f1695256e67e6f Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Thu, 15 Jan 2026 18:01:06 +0800 Subject: [PATCH 11/21] feat(linovelib): use external server to extract shuffle coefficients --- plugins/chinese/linovelib.ts | 63 ++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 969daa506..00f9a1398 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -17,8 +17,42 @@ type FetchInit = { | Headers; }; +type Coefficients = { + lcgModulus: number; + lcgMultiplier: number; + lcgIncrement: number; + seedMultiplier: number; + seedOffset: number; +}; + +const coefficientCache = { + data: null as Coefficients | null, + lastVersion: '', +}; + +function extractChapterLogScriptUrl($: CheerioAPI): string { + return ( + $( + $('script') + .toArray() + .find(el => { + const scriptContent = $(el).attr('src') || ''; + return /chapterlog\.js/.test(scriptContent); + }), + ).attr('src') || '' + ); +} + +function isCacheValid($: CheerioAPI): boolean { + const scriptUrl = extractChapterLogScriptUrl($); + const version = scriptUrl.match(/chapterlog\.js\?(v.*)/)?.[1] || ''; + const lastVersion = coefficientCache.lastVersion; + const isSameVersion = version === lastVersion; + if (!isSameVersion) coefficientCache.lastVersion = version; + return isSameVersion; +} export class LinovelibDecrpytor { - public static decrypt($: CheerioAPI): string { + public static async decrypt($: CheerioAPI): Promise { const chapterId = this.extractChapterId($); const container = $('#acontent'); if (!container.length) return ''; @@ -44,6 +78,27 @@ export class LinovelibDecrpytor { container.find('div.co').remove(); + const coefficients = await (async (): Promise => { + if (isCacheValid($) && coefficientCache.data) { + return coefficientCache.data; + } else { + // Fetch shuffle coefficients from the remote server hosted by constasj + // As the extraction of these coefficients from linovelib's chapterlog.js requires webcrack and babel, and webcrack requires isolated-vm, which needs native compilation + // which is impossible to be done in this plugin's limited JavaScript runtime. + // You can view the source code of server at https://github.com/ConstasJ/linovelib-descramble-server + const text = await fetchText('https://lds.constasj.me/coefficients', { + method: 'GET', + hedaers: { + 'Accept': 'application/json', + }, + }); + console.log(text); + const resObj = JSON.parse(text) as { coefficients: Coefficients }; + const coefficients = resObj.coefficients; + return (coefficientCache.data = coefficients); + } + })(); + const allChildren = container .contents() .toArray() @@ -65,7 +120,9 @@ export class LinovelibDecrpytor { return container.html() || ''; } - const seed = parseInt(chapterId, 10) * 127 + 235; + const seed = + parseInt(chapterId, 10) * coefficients.seedMultiplier + + coefficients.seedOffset; const dynamicIndices = Array.from( { length: pCount - 20 }, @@ -529,7 +586,7 @@ class Linovelib implements Plugin.PluginBase { }; const addPage = async (pageCheerio: CheerioAPI) => { const formatPage = async () => { - pageText = LinovelibDecrpytor.decrypt(pageCheerio).replace( + pageText = (await LinovelibDecrpytor.decrypt(pageCheerio)).replace( /./g, char => skillgg[char] || char, ); From 748a0c8a5f258aa65e74c01b6c599e2459cc56b0 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Thu, 15 Jan 2026 18:31:08 +0800 Subject: [PATCH 12/21] fix(linovelib): extract author name more precisely --- plugins/chinese/linovelib.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 00f9a1398..464b0b592 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -284,7 +284,9 @@ class Linovelib implements Plugin.PluginBase { novel.summary = loadedCheerio('#bookSummary content').text(); - novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a a').text(); + novel.author = loadedCheerio( + '#bookDetailWrapper .book-rand-a .authorname a', + ).text(); const meta = loadedCheerio('#bookDetailWrapper .book-meta').text(); novel.status = meta.includes('完结') From becbdbe52c77989fa94541dccd82a25d1fe6e149 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Sat, 17 Jan 2026 01:48:28 +0800 Subject: [PATCH 13/21] fix(linovelib): set Referer header to bypass anti-leeching/hotlinking of image source Also, use LocalStorage to store coefficients and chapter.js version --- plugins/chinese/linovelib.ts | 72 ++++++++++++++---------------------- plugins/index.ts | 2 +- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 464b0b592..3f10a3ae6 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -1,5 +1,5 @@ import { CheerioAPI, load as parseHTML } from 'cheerio'; -import { fetchText as ft } from '@libs/fetch'; +import { fetchText } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; @@ -25,11 +25,6 @@ type Coefficients = { seedOffset: number; }; -const coefficientCache = { - data: null as Coefficients | null, - lastVersion: '', -}; - function extractChapterLogScriptUrl($: CheerioAPI): string { return ( $( @@ -46,9 +41,12 @@ function extractChapterLogScriptUrl($: CheerioAPI): string { function isCacheValid($: CheerioAPI): boolean { const scriptUrl = extractChapterLogScriptUrl($); const version = scriptUrl.match(/chapterlog\.js\?(v.*)/)?.[1] || ''; - const lastVersion = coefficientCache.lastVersion; + const lastVersion = + localStorage.getItem('linovelib_chapterlogjs_version') || ''; const isSameVersion = version === lastVersion; - if (!isSameVersion) coefficientCache.lastVersion = version; + if (!isSameVersion) { + localStorage.setItem('linovelib_chapterlogjs_version', version); + } return isSameVersion; } export class LinovelibDecrpytor { @@ -79,8 +77,11 @@ export class LinovelibDecrpytor { container.find('div.co').remove(); const coefficients = await (async (): Promise => { - if (isCacheValid($) && coefficientCache.data) { - return coefficientCache.data; + const coefficients: Coefficients | undefined = JSON.parse( + localStorage.getItem('linovelib_shuffle_coefficients') || 'null', + ); + if (isCacheValid($) && coefficients) { + return coefficients; } else { // Fetch shuffle coefficients from the remote server hosted by constasj // As the extraction of these coefficients from linovelib's chapterlog.js requires webcrack and babel, and webcrack requires isolated-vm, which needs native compilation @@ -95,7 +96,11 @@ export class LinovelibDecrpytor { console.log(text); const resObj = JSON.parse(text) as { coefficients: Coefficients }; const coefficients = resObj.coefficients; - return (coefficientCache.data = coefficients); + localStorage.setItem( + 'linovelib_shuffle_coefficients', + JSON.stringify(coefficients), + ); + return coefficients; } })(); @@ -186,44 +191,23 @@ export class LinovelibDecrpytor { return chapterId; } } - -async function fetchText(url: string, init?: FetchInit) { - const actInit = (() => { - if (init?.headers) { - if (init.headers instanceof Headers) { - if (!init.headers.get('User-Agent')) { - init.headers.set( - 'User-Agent', - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - ); - } - } else { - init.headers = { - 'User-Agent': - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - ...init.headers, - }; - } - } else { - init = { - ...init, - headers: { - 'User-Agent': - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - }, - }; - } - return init; - })(); - return await ft(url, actInit); -} - class Linovelib implements Plugin.PluginBase { id = 'linovelib'; name = 'Linovelib'; icon = 'src/cn/linovelib/icon.png'; site = 'https://www.bilinovel.com'; - version = '1.2.0'; + version = '1.2.1'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = { + method: 'GET', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', + 'Referer': this.site, + 'Accept': + 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + }, + }; + webStorageUtilized = true; async popularNovels( pageNo: number, diff --git a/plugins/index.ts b/plugins/index.ts index a765c7e1b..4152a37f8 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -58,7 +58,7 @@ import p_55 from '@plugins/english/HiraethTranslation[madara]'; import p_56 from '@plugins/english/HotNovelPub[hotnovelpub]'; import p_57 from '@plugins/english/inkitt'; import p_58 from '@plugins/english/Ippotranslations[lightnovelwp]'; -import p_59 from '@plugins/english/kdtnovels'; +import p_59 from '@plugins/english/KDTNovels[lightnovelwp]'; import p_60 from '@plugins/english/KeopiTranslations[lightnovelwp]'; import p_61 from '@plugins/english/KnoxT[lightnovelwp]'; import p_62 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; From c679fd07dbf583fc486b9f5944115c134acbfc37 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Sat, 17 Jan 2026 01:50:12 +0800 Subject: [PATCH 14/21] refactor: delete FetchInit definition in this plugin --- plugins/chinese/linovelib.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 3f10a3ae6..50abc823a 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -5,18 +5,6 @@ import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { type Element, type AnyNode } from 'domhandler'; -type FetchInit = { - headers?: Record | Headers; - method?: string; - body?: FormData | string; - [x: string]: - | string - | Record - | undefined - | FormData - | Headers; -}; - type Coefficients = { lcgModulus: number; lcgMultiplier: number; @@ -49,7 +37,7 @@ function isCacheValid($: CheerioAPI): boolean { } return isSameVersion; } -export class LinovelibDecrpytor { +class LinovelibDecrpytor { public static async decrypt($: CheerioAPI): Promise { const chapterId = this.extractChapterId($); const container = $('#acontent'); From b2e22ef569e0422ffba0d46f03e0fc14d7debfa6 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Sat, 17 Jan 2026 02:03:50 +0800 Subject: [PATCH 15/21] fix(linovelib): replace localStorage with storage utility for better data management --- plugins/chinese/linovelib.ts | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 50abc823a..9a260f980 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -4,6 +4,7 @@ import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { type Element, type AnyNode } from 'domhandler'; +import { storage } from '@/lib/storage'; type Coefficients = { lcgModulus: number; @@ -29,11 +30,10 @@ function extractChapterLogScriptUrl($: CheerioAPI): string { function isCacheValid($: CheerioAPI): boolean { const scriptUrl = extractChapterLogScriptUrl($); const version = scriptUrl.match(/chapterlog\.js\?(v.*)/)?.[1] || ''; - const lastVersion = - localStorage.getItem('linovelib_chapterlogjs_version') || ''; + const lastVersion = storage.get('linovelib_chapterlogjs_version') || ''; const isSameVersion = version === lastVersion; if (!isSameVersion) { - localStorage.setItem('linovelib_chapterlogjs_version', version); + storage.set('linovelib_chapterlogjs_version', version); } return isSameVersion; } @@ -65,17 +65,17 @@ class LinovelibDecrpytor { container.find('div.co').remove(); const coefficients = await (async (): Promise => { - const coefficients: Coefficients | undefined = JSON.parse( - localStorage.getItem('linovelib_shuffle_coefficients') || 'null', + const coefficients: Coefficients | null = storage.get( + 'linovelib_shuffle_coefficients', ); if (isCacheValid($) && coefficients) { return coefficients; } else { - // Fetch shuffle coefficients from the remote server hosted by constasj + // Fetch shuffle coefficients from the lds server by url sets by a user // As the extraction of these coefficients from linovelib's chapterlog.js requires webcrack and babel, and webcrack requires isolated-vm, which needs native compilation // which is impossible to be done in this plugin's limited JavaScript runtime. - // You can view the source code of server at https://github.com/ConstasJ/linovelib-descramble-server - const text = await fetchText('https://lds.constasj.me/coefficients', { + // You can view the source code of the lds server at https://github.com/ConstasJ/linovelib-descramble-server + const text = await fetchText(`${storage.get('host')}/coefficients`, { method: 'GET', hedaers: { 'Accept': 'application/json', @@ -84,10 +84,7 @@ class LinovelibDecrpytor { console.log(text); const resObj = JSON.parse(text) as { coefficients: Coefficients }; const coefficients = resObj.coefficients; - localStorage.setItem( - 'linovelib_shuffle_coefficients', - JSON.stringify(coefficients), - ); + storage.set('linovelib_shuffle_coefficients', coefficients); return coefficients; } })(); @@ -184,7 +181,7 @@ class Linovelib implements Plugin.PluginBase { name = 'Linovelib'; icon = 'src/cn/linovelib/icon.png'; site = 'https://www.bilinovel.com'; - version = '1.2.1'; + version = '1.2.0'; imageRequestInit?: Plugin.ImageRequestInit | undefined = { method: 'GET', headers: { @@ -196,6 +193,14 @@ class Linovelib implements Plugin.PluginBase { }, }; webStorageUtilized = true; + pluginSettings = { + host: { + value: '', + label: + 'Custom Linovelib Descramble Server Host (starts with http:// or https://)', + type: 'Text', + }, + }; async popularNovels( pageNo: number, From bd8e62e157ede2885ce3a7229fa575c88f2ec9af Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Sat, 17 Jan 2026 02:13:47 +0800 Subject: [PATCH 16/21] fix(linovelib): update storage import path to use the correct library --- plugins/chinese/linovelib.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 9a260f980..e0f93c134 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -4,7 +4,7 @@ import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; import { NovelStatus } from '@libs/novelStatus'; import { type Element, type AnyNode } from 'domhandler'; -import { storage } from '@/lib/storage'; +import { storage } from '@libs/storage'; type Coefficients = { lcgModulus: number; From dad966745bf377d69a54878f6e0250a7516e03fe Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Sat, 17 Jan 2026 14:46:00 +0800 Subject: [PATCH 17/21] feat(linovelib): Implement chapter number --- plugins/chinese/linovelib.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index e0f93c134..63493e69d 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -289,6 +289,8 @@ class Linovelib implements Plugin.PluginBase { let volumeName: string, chapterId: number; + let chapterCounter = 0; + chaptersLoadedCheerio('#volumes .chapter-li:not(.volume-cover)').each( (i, el) => { if (chaptersLoadedCheerio(el).hasClass('chapter-bar')) { @@ -325,10 +327,13 @@ class Linovelib implements Plugin.PluginBase { if (!chapterId) return; + chapterCounter++; + chapter.push({ name: chapterName, releaseTime: releaseDate, path: chapterUrl, + chapterNumber: chapterCounter, }); }, ); From 044d015f0a8a4f76d5945852ad2972989cb1ab85 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Mon, 19 Jan 2026 12:49:46 +0800 Subject: [PATCH 18/21] refactor(linovelib): as mobile site equipped with strong cloudflare challenge, move most of parsing logic to LDS server --- plugins/chinese/linovelib.ts | 655 ++--------------------------------- 1 file changed, 32 insertions(+), 623 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 63493e69d..7bbb7e39b 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -1,181 +1,9 @@ -import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { load as parseHTML } from 'cheerio'; import { fetchText } from '@libs/fetch'; import { FilterTypes, Filters } from '@libs/filterInputs'; import { Plugin } from '@/types/plugin'; -import { NovelStatus } from '@libs/novelStatus'; -import { type Element, type AnyNode } from 'domhandler'; import { storage } from '@libs/storage'; -type Coefficients = { - lcgModulus: number; - lcgMultiplier: number; - lcgIncrement: number; - seedMultiplier: number; - seedOffset: number; -}; - -function extractChapterLogScriptUrl($: CheerioAPI): string { - return ( - $( - $('script') - .toArray() - .find(el => { - const scriptContent = $(el).attr('src') || ''; - return /chapterlog\.js/.test(scriptContent); - }), - ).attr('src') || '' - ); -} - -function isCacheValid($: CheerioAPI): boolean { - const scriptUrl = extractChapterLogScriptUrl($); - const version = scriptUrl.match(/chapterlog\.js\?(v.*)/)?.[1] || ''; - const lastVersion = storage.get('linovelib_chapterlogjs_version') || ''; - const isSameVersion = version === lastVersion; - if (!isSameVersion) { - storage.set('linovelib_chapterlogjs_version', version); - } - return isSameVersion; -} -class LinovelibDecrpytor { - public static async decrypt($: CheerioAPI): Promise { - const chapterId = this.extractChapterId($); - const container = $('#acontent'); - if (!container.length) return ''; - - container.find('p').each((_, el) => { - const $el = $(el); - const innerHtml = $el.html(); - if (innerHtml) { - const cleanedHtml = innerHtml.replace(/^\s+|(?<=>)\s+/g, ''); - $el.html(cleanedHtml); - } - }); - - container.find('img.imagecontent').each((_, el) => { - const imgSrc = $(el).attr('data-src') || $(el).attr('src'); - if (imgSrc) { - $(el) - .attr('src', imgSrc) - .removeAttr('data-src') - .removeClass('lazyload'); - } - }); - - container.find('div.co').remove(); - - const coefficients = await (async (): Promise => { - const coefficients: Coefficients | null = storage.get( - 'linovelib_shuffle_coefficients', - ); - if (isCacheValid($) && coefficients) { - return coefficients; - } else { - // Fetch shuffle coefficients from the lds server by url sets by a user - // As the extraction of these coefficients from linovelib's chapterlog.js requires webcrack and babel, and webcrack requires isolated-vm, which needs native compilation - // which is impossible to be done in this plugin's limited JavaScript runtime. - // You can view the source code of the lds server at https://github.com/ConstasJ/linovelib-descramble-server - const text = await fetchText(`${storage.get('host')}/coefficients`, { - method: 'GET', - hedaers: { - 'Accept': 'application/json', - }, - }); - console.log(text); - const resObj = JSON.parse(text) as { coefficients: Coefficients }; - const coefficients = resObj.coefficients; - storage.set('linovelib_shuffle_coefficients', coefficients); - return coefficients; - } - })(); - - const allChildren = container - .contents() - .toArray() - .filter(node => !(node.type === 'tag' && node.tagName === 'div')); - - const sortableEntries: { element: Element; originalPos: number }[] = []; - - allChildren.forEach((node, index) => { - if (node.type === 'tag' && node.tagName === 'p') { - const text = $(node).text().trim(); - if (text.length > 0) { - sortableEntries.push({ element: node, originalPos: index }); - } - } - }); - - const pCount = sortableEntries.length; - if (pCount <= 20) { - return container.html() || ''; - } - - const seed = - parseInt(chapterId, 10) * coefficients.seedMultiplier + - coefficients.seedOffset; - - const dynamicIndices = Array.from( - { length: pCount - 20 }, - (_, i) => i + 20, - ); - const shuffledIndices = this.shuffle(dynamicIndices, seed); - - const fullMapping = Array.from({ length: 20 }, (_, i) => i).concat( - shuffledIndices, - ); - - const restoredChildren: (AnyNode | null)[] = [...allChildren]; - - sortableEntries.forEach((entry, i) => { - const targetLogicalPos = fullMapping[i]; - const actualSlot = sortableEntries[targetLogicalPos]?.originalPos; - restoredChildren[actualSlot] = entry.element; - }); - - const newContainer = $('
'); - restoredChildren.forEach(node => { - if (node && node.type === 'tag') { - newContainer.append($(node)); - newContainer.append('\n'); - } - }); - - return newContainer.html() || ''; - } - - private static shuffle(array: number[], seed: number): number[] { - let currentSeed = seed; - const result = [...array]; - const len = result.length; - - for (let i = len - 1; i > 0; i--) { - currentSeed = (currentSeed * 9302 + 49397) % 233280; - const j = Math.floor((currentSeed / 233280) * (i + 1)); - - // 交换 - const temp = result[i]; - result[i] = result[j]; - result[j] = temp; - } - return result; - } - - private static extractChapterId($: CheerioAPI): string { - const scriptTags = $('script'); - let chapterId = ''; - scriptTags.each((_, el) => { - const scriptContent = $(el).html(); - if (scriptContent) { - const match = scriptContent.match(/chapterid\s*:\s*'(\d+)'/); - if (match && match[1]) { - chapterId = match[1]; - return false; // Break the loop - } - } - }); - return chapterId; - } -} class Linovelib implements Plugin.PluginBase { id = 'linovelib'; name = 'Linovelib'; @@ -186,8 +14,8 @@ class Linovelib implements Plugin.PluginBase { method: 'GET', headers: { 'User-Agent': - 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36', - 'Referer': this.site, + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0', + 'Referer': 'https://www.linovelib.com', 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', }, @@ -195,9 +23,8 @@ class Linovelib implements Plugin.PluginBase { webStorageUtilized = true; pluginSettings = { host: { - value: '', - label: - 'Custom Linovelib Descramble Server Host (starts with http:// or https://)', + value: 'http://example.com', + label: 'Custom LDS Host', type: 'Text', }, }; @@ -242,461 +69,43 @@ class Linovelib implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const url = this.site + novelPath; - - const body = await fetchText(url); - if (body === '') throw Error('无法获取小说内容,请检查网络'); - - const loadedCheerio = parseHTML(body); - - const novel: Plugin.SourceNovel = { - path: novelPath, - chapters: [], - name: loadedCheerio('#bookDetailWrapper .book-title').text(), - }; - - novel.cover = loadedCheerio('#bookDetailWrapper img.book-cover').attr( - 'src', + const serverUrl = storage.get('host'); + return JSON.parse( + await fetchText(`${serverUrl}/api/novel`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ path: novelPath }), + }), ); - - novel.summary = loadedCheerio('#bookSummary content').text(); - - novel.author = loadedCheerio( - '#bookDetailWrapper .book-rand-a .authorname a', - ).text(); - - const meta = loadedCheerio('#bookDetailWrapper .book-meta').text(); - novel.status = meta.includes('完结') - ? NovelStatus.Completed - : NovelStatus.Ongoing; - - novel.genres = loadedCheerio('.tag-small.red') - .children('a') - .map((i, el) => loadedCheerio(el).text()) - .toArray() - .join(','); - - // Table of Content is on a different page than the summary page - const chapter: Plugin.ChapterItem[] = []; - - const idPattern = /\/(\d+)\.html/; - const novelId = url.match(idPattern)?.[1]; - - const chaptersUrl = this.site + loadedCheerio('#btnReadBook').attr('href'); - const chaptersBody = await fetchText(chaptersUrl); - - const chaptersLoadedCheerio = parseHTML(chaptersBody); - - let volumeName: string, chapterId: number; - - let chapterCounter = 0; - - chaptersLoadedCheerio('#volumes .chapter-li:not(.volume-cover)').each( - (i, el) => { - if (chaptersLoadedCheerio(el).hasClass('chapter-bar')) { - volumeName = chaptersLoadedCheerio(el).text(); - return; - } else { - const urlPart = chaptersLoadedCheerio(el) - .find('.chapter-li-a') - .attr('href'); - const chapterIdMatch = urlPart?.match(idPattern); - - // Sometimes the href attribute does not contain the url, but javascript:cid(0). - // Increment the previous chapter ID should result in the right URL - if (chapterIdMatch) { - chapterId = +chapterIdMatch[1]; - } else { - chapterId++; - } - } - - const chapterUrl = `/novel/${novelId}/${chapterId}.html`; - const chapNameTransDict: Record = { - '\u004e': '\u5973', - }; - const chapterName = - volumeName + - ' — ' + - chaptersLoadedCheerio(el) - .find('.chapter-index') - .text() - .trim() - .replace(/./g, char => chapNameTransDict[char] || char); - const releaseDate = null; - - if (!chapterId) return; - - chapterCounter++; - - chapter.push({ - name: chapterName, - releaseTime: releaseDate, - path: chapterUrl, - chapterNumber: chapterCounter, - }); - }, - ); - - novel.chapters = chapter; - - return novel; } async parseChapter(chapterPath: string): Promise { - let chapterName, - chapterText = '', - hasNextPage, - pageHasNextPage, - pageText = ''; - let pageNumber = 1; - - /* - * TODO: Maybe there are other ways to get the translation table - * It is embed and encrypted inside readtool.js - * UPDATE: Decrypted, see skillgg - */ - // const mapping_dict = { - // '“': '「', - // '’': '』', - // '': '是', - // '': '不', - // '': '好', - // '': '个', - // '': '开', - // '': '样', - // '': '想', - // '': '说', - // '': '年', - // '': '那', - // '': '她', - // '': '美', - // '': '自', - // '': '家', - // '': '而', - // '': '去', - // '': '都', - // '': '于', - // '': '舔', - // '': '他', - // '': '只', - // '': '看', - // '': '来', - // '': '用', - // '': '道', - // '': '得', - // '': '乳', - // '': '茎', - // '': '肉', - // '': '胸', - // '': '淫', - // '': '性', - // '': '骚', - // '”': '」', - // '': '的', - // '': '当', - // '': '人', - // '': '有', - // '': '上', - // '': '到', - // '': '地', - // '': '中', - // '': '生', - // '': '着', - // '': '和', - // '': '起', - // '': '交', - // '': '以', - // '': '可', - // '': '过', - // '': '能', - // '': '多', - // '': '心', - // '': '小', - // '': '成', - // '': '了', - // '': '把', - // '': '发', - // '': '第', - // '': '子', - // '': '事', - // '': '阴', - // '': '欲', - // '': '里', - // '': '私', - // '': '臀', - // '': '脱', - // '': '唇', - // '‘': '『', - // '': '一', - // '': '我', - // '': '在', - // '': '这', - // '': '们', - // '': '时', - // '': '为', - // '': '你', - // '': '国', - // '': '就', - // '': '要', - // '': '也', - // '': '后', - // '': '没', - // '': '下', - // '': '天', - // '': '对', - // '': '然', - // '': '学', - // '': '之', - // '': '出', - // '': '没', - // '': '如', - // '': '还', - // '': '大', - // '': '作', - // '': '种', - // '': '液', - // '': '呻', - // '': '射', - // '': '穴', - // '': '么', - // '': '裸', - // }; - const skillgg: Record = { - '\u201c': '\u300c', - '\u201d': '\u300d', - '\u2018': '\u300e', - '\u2019': '\u300f', - '\ue82c': '\u7684', - '\ue852': '\u4e00', - '\ue82d': '\u662f', - '\ue819': '\u4e86', - '\ue856': '\u6211', - '\ue857': '\u4e0d', - '\ue816': '\u4eba', - '\ue83c': '\u5728', - '\ue830': '\u4ed6', - '\ue82e': '\u6709', - '\ue836': '\u8fd9', - '\ue859': '\u4e2a', - '\ue80a': '\u4e0a', - '\ue855': '\u4eec', - '\ue842': '\u6765', - '\ue858': '\u5230', - '\ue80b': '\u65f6', - '\ue81f': '\u5927', - '\ue84a': '\u5730', - '\ue853': '\u4e3a', - '\ue81e': '\u5b50', - '\ue822': '\u4e2d', - '\ue813': '\u4f60', - '\ue85b': '\u8bf4', - '\ue807': '\u751f', - '\ue818': '\u56fd', - '\ue810': '\u5e74', - '\ue812': '\u7740', - '\ue851': '\u5c31', - '\ue801': '\u90a3', - '\ue80c': '\u548c', - '\ue815': '\u8981', - '\ue84c': '\u5979', - '\ue840': '\u51fa', - '\ue848': '\u4e5f', - '\ue835': '\u5f97', - '\ue800': '\u91cc', - '\ue826': '\u540e', - '\ue863': '\u81ea', - '\ue861': '\u4ee5', - '\ue854': '\u4f1a', - '\ue827': '\u5bb6', - '\ue83b': '\u53ef', - '\ue85d': '\u4e0b', - '\ue84d': '\u800c', - '\ue862': '\u8fc7', - '\ue81c': '\u5929', - '\ue81d': '\u53bb', - '\ue860': '\u80fd', - '\ue843': '\u5bf9', - '\ue82f': '\u5c0f', - '\ue802': '\u591a', - '\ue831': '\u7136', - '\ue84b': '\u4e8e', - '\ue837': '\u5fc3', - '\ue829': '\u5b66', - '\ue85e': '\u4e48', - '\ue83a': '\u4e4b', - '\ue832': '\u90fd', - '\ue808': '\u597d', - '\ue841': '\u770b', - '\ue821': '\u8d77', - '\ue845': '\u53d1', - '\ue803': '\u5f53', - '\ue828': '\u6ca1', - '\ue81b': '\u6210', - '\ue83e': '\u53ea', - '\ue820': '\u5982', - '\ue84e': '\u4e8b', - '\ue85a': '\u628a', - '\ue806': '\u8fd8', - '\ue83f': '\u7528', - '\ue833': '\u7b2c', - '\ue811': '\u6837', - '\ue804': '\u9053', - '\ue814': '\u60f3', - '\ue80f': '\u4f5c', - '\ue84f': '\u79cd', - '\ue80e': '\u5f00', - '\ue823': '\u7f8e', - '\ue849': '\u4e73', - '\ue805': '\u9634', - '\ue809': '\u6db2', - '\ue81a': '\u830e', - '\ue844': '\u6b32', - '\ue847': '\u547b', - '\ue850': '\u8089', - '\ue824': '\u4ea4', - '\ue85f': '\u6027', - '\ue817': '\u80f8', - '\ue85c': '\u79c1', - '\ue838': '\u7a74', - '\ue82a': '\u6deb', - '\ue83d': '\u81c0', - '\ue82b': '\u8214', - '\ue80d': '\u5c04', - '\ue839': '\u8131', - '\ue834': '\u88f8', - '\ue846': '\u9a9a', - '\ue825': '\u5507', - }; - const addPage = async (pageCheerio: CheerioAPI) => { - const formatPage = async () => { - pageText = (await LinovelibDecrpytor.decrypt(pageCheerio)).replace( - /./g, - char => skillgg[char] || char, - ); - - return Promise.resolve(); - }; - - await formatPage(); - chapterName = - pageCheerio('#atitle + h3').text() + - ' — ' + - pageCheerio('#atitle').text(); - if (chapterText === '') { - chapterText = '

' + chapterName + '

'; - } - chapterText += pageText; - }; - - const loadPage = async (url: string) => { - const headers = { - 'Accept': - 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', - 'Accept-Language': - 'zh-CN,zh;q=0.9,zh-TW;q=0.8,zh-HK;q=0.7,en;q=0.6,en-GB;q=0.5,en-US;q=0.4', - 'Cache-Control': 'no-cache', - }; - - const body = await fetchText(url, { headers }); - const pageCheerio = parseHTML(body); - await addPage(pageCheerio); - pageHasNextPage = - pageCheerio('#footlink a:last').text() === '下一页' || - pageCheerio('#footlink a:last').text() === '下一頁' - ? true - : false; - return { pageCheerio, pageHasNextPage }; - }; - - let url = this.site + chapterPath; - const baseUrl = url; - do { - const page = await loadPage(url); - hasNextPage = page.pageHasNextPage; - if (hasNextPage === true) { - pageNumber++; - url = baseUrl.replace(/\.html/gi, `_${pageNumber}` + '.html'); - } - } while (hasNextPage === true); - return chapterText; + const serverUrl = storage.get('host'); + return await fetchText(`${serverUrl}/api/chapter`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ path: chapterPath }), + }); } async searchNovels( searchTerm: string, pageNo: number, ): Promise { - const url = `${this.site}/search.html?searchkey=${encodeURIComponent(searchTerm)}`; - - const body = await fetchText(url, { - headers: { - 'Referer': url, - 'Cookie': 'night=0', - }, - }); - - if (body === '') throw Error('无法获取搜索结果,请检查网络'); - - const pageCheerio = parseHTML(body); - - const novels: Plugin.NovelItem[] = []; - - const loadSearchResults = () => { - pageCheerio('.book-ol .book-layout').each((i, el) => { - const nUrl = pageCheerio(el).attr('href')?.replace(this.site, ''); - - const novelName = pageCheerio(el).find('.book-title').text(); - const novelCover = pageCheerio(el) - .find('div.book-cover > img') - .attr('data-src'); - const novelUrl = nUrl || ''; - - if (!nUrl) return; - - novels.push({ - name: novelName, - path: novelUrl, - cover: novelCover, - }); - }); - }; - - const addPage = async (pageCheerio: CheerioAPI, redirect: string) => { - const novelResults = pageCheerio('.book-ol a.book-layout'); - if (novelResults.length === 0) { - // No results found, nothing to do - } else { - loadSearchResults(); - } - - if (redirect.length) { - novels.length = 0; - const novelName = pageCheerio('.book-detail-info .book-title').text(); - - const novelCover = pageCheerio( - '.book-detail-info div.module-item-cover > img', - ).attr('src'); - const novelUrl = - pageCheerio('#btnReadBook').attr('href')?.slice(0, -8) + '.html'; - novels.push({ - name: novelName, - path: novelUrl, - cover: novelCover, - }); - } - }; - - // NOTE: don't know redirect is for what, comment out for now - // Note: Found that Linovelib will redirect to the novel page if there's only one result,so uncommenting this out - - const redirect = pageCheerio('div.book-layout').text(); - if (redirect.length > 0) { - await addPage(pageCheerio, redirect); - } else { - loadSearchResults(); - } - - return novels; + const serverUrl = storage.get('host'); + return await JSON.parse( + await fetchText(`${serverUrl}/api/search`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ keyword: searchTerm }), + }), + ); } filters = { From 5f83bc4e0e8f5fba4cb9654002c17d4b089bd87c Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Mon, 19 Jan 2026 13:41:27 +0800 Subject: [PATCH 19/21] fix(linovelib): update API calls to use query parameters instead of POST requests for novel, chapter, and search endpoints --- plugins/chinese/linovelib.ts | 34 ++++++++++------------------------ 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 7bbb7e39b..39f1f7342 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -70,26 +70,16 @@ class Linovelib implements Plugin.PluginBase { async parseNovel(novelPath: string): Promise { const serverUrl = storage.get('host'); - return JSON.parse( - await fetchText(`${serverUrl}/api/novel`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ path: novelPath }), - }), - ); + const res = await fetchText(`${serverUrl}/api/novel?path=${novelPath}`); + const novel = JSON.parse(res) as Plugin.SourceNovel; + return novel; } async parseChapter(chapterPath: string): Promise { const serverUrl = storage.get('host'); - return await fetchText(`${serverUrl}/api/chapter`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ path: chapterPath }), - }); + const res = await fetchText(`${serverUrl}/api/chapter?path=${chapterPath}`); + const resObj = JSON.parse(res); + return resObj.content; } async searchNovels( @@ -97,15 +87,11 @@ class Linovelib implements Plugin.PluginBase { pageNo: number, ): Promise { const serverUrl = storage.get('host'); - return await JSON.parse( - await fetchText(`${serverUrl}/api/search`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ keyword: searchTerm }), - }), + const res = await fetchText( + `${serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`, ); + const novelsData = JSON.parse(res).results as Plugin.NovelItem[]; + return novelsData; } filters = { From 9cd19f32b7c7156ef26b7154503fc07c38785c9f Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Mon, 19 Jan 2026 14:39:09 +0800 Subject: [PATCH 20/21] fix(linovelib): add caching mechanism for novel and chapter parsing to reduce API calls --- plugins/chinese/linovelib.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 39f1f7342..4db290215 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -69,16 +69,23 @@ class Linovelib implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const serverUrl = storage.get('host'); + const serverUrl = storage.get('host') || 'https://lds.constasj.me'; const res = await fetchText(`${serverUrl}/api/novel?path=${novelPath}`); const novel = JSON.parse(res) as Plugin.SourceNovel; return novel; } async parseChapter(chapterPath: string): Promise { - const serverUrl = storage.get('host'); + const lastFetchChapterTime = + storage.get('lastFetchChapterTime_' + chapterPath) || 0; + if (Date.now() - lastFetchChapterTime < 10000) { + return storage.get('chapterContent_' + chapterPath) || ''; + } + const serverUrl = storage.get('host') || 'https://lds.constasj.me'; const res = await fetchText(`${serverUrl}/api/chapter?path=${chapterPath}`); const resObj = JSON.parse(res); + storage.set('lastFetchChapterTime_' + chapterPath, Date.now()); + storage.set('chapterContent_' + chapterPath, resObj.content); return resObj.content; } @@ -86,11 +93,16 @@ class Linovelib implements Plugin.PluginBase { searchTerm: string, pageNo: number, ): Promise { - const serverUrl = storage.get('host'); + const lastSearchTime = storage.get('lastSearchTime_' + this.id) || 0; + if (Date.now() - lastSearchTime < 5000) { + return []; + } + const serverUrl = storage.get('host') || 'https://lds.constasj.me'; const res = await fetchText( `${serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`, ); const novelsData = JSON.parse(res).results as Plugin.NovelItem[]; + storage.set('lastSearchTime_' + this.id, Date.now()); return novelsData; } From 764755b8e6f8592d470da6194e240e3c9d4322e3 Mon Sep 17 00:00:00 2001 From: ConstasJ Date: Tue, 20 Jan 2026 00:02:38 +0800 Subject: [PATCH 21/21] refactor(linovelib): refactor server URL handling to use class property for API requests --- plugins/chinese/linovelib.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts index 4db290215..30ae3d64a 100644 --- a/plugins/chinese/linovelib.ts +++ b/plugins/chinese/linovelib.ts @@ -28,6 +28,7 @@ class Linovelib implements Plugin.PluginBase { type: 'Text', }, }; + serverUrl = storage.get('host') || 'http://localhost:5301'; async popularNovels( pageNo: number, @@ -69,8 +70,9 @@ class Linovelib implements Plugin.PluginBase { } async parseNovel(novelPath: string): Promise { - const serverUrl = storage.get('host') || 'https://lds.constasj.me'; - const res = await fetchText(`${serverUrl}/api/novel?path=${novelPath}`); + const res = await fetchText( + `${this.serverUrl}/api/novel?path=${novelPath}`, + ); const novel = JSON.parse(res) as Plugin.SourceNovel; return novel; } @@ -81,8 +83,9 @@ class Linovelib implements Plugin.PluginBase { if (Date.now() - lastFetchChapterTime < 10000) { return storage.get('chapterContent_' + chapterPath) || ''; } - const serverUrl = storage.get('host') || 'https://lds.constasj.me'; - const res = await fetchText(`${serverUrl}/api/chapter?path=${chapterPath}`); + const res = await fetchText( + `${this.serverUrl}/api/chapter?path=${chapterPath}`, + ); const resObj = JSON.parse(res); storage.set('lastFetchChapterTime_' + chapterPath, Date.now()); storage.set('chapterContent_' + chapterPath, resObj.content); @@ -93,13 +96,13 @@ class Linovelib implements Plugin.PluginBase { searchTerm: string, pageNo: number, ): Promise { + console.log(pageNo); const lastSearchTime = storage.get('lastSearchTime_' + this.id) || 0; if (Date.now() - lastSearchTime < 5000) { return []; } - const serverUrl = storage.get('host') || 'https://lds.constasj.me'; const res = await fetchText( - `${serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`, + `${this.serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`, ); const novelsData = JSON.parse(res).results as Plugin.NovelItem[]; storage.set('lastSearchTime_' + this.id, Date.now());