diff --git a/.env.template b/.env.template new file mode 100644 index 000000000..3f25bb9a9 --- /dev/null +++ b/.env.template @@ -0,0 +1,7 @@ +# Override the default user content base path +# Default when empty: https://raw.githubusercontent.com/${USERNAME}/${REPO}/${CURRENT_BRANCH} +# +# for android emulator use 10.0.2.2 +# for iOS simulator use 127.0.0.1 or localhost +# for a real device use the IP address of your computer in the local network (e.g. 192.168.21.37) +USER_CONTENT_BASE=http://localhost:3000 diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml new file mode 100644 index 000000000..f3f09baa5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -0,0 +1,371 @@ +# DO NOT EDIT THIS FILE DIRECTLY +# This file is auto-generated from blank_report_issue.yml in .github/scripts/ +# Any changes made here will be overwritten when plugins are published. + +name: Report Issue +description: Report a plugin issue in LNReader +labels: [Bug] +body: + - type: dropdown + id: source + attributes: + label: Plugin + description: Select the affected plugin(s). + multiple: true + options: + - "‎العربية: ArNovel" + - "‎العربية: Azora" + - "‎العربية: dilar tube" + - "‎العربية: Free Kol Novel" + - "‎العربية: HizoManga" + - "‎العربية: Kol Novel" + - "‎العربية: Markazriwayat" + - "‎العربية: Novel4Up" + - "‎العربية: Novels Paradise" + - "‎العربية: Olaoe.cyou" + - "‎العربية: Rewayat Club" + - "‎العربية: Riwyat" + - "‎العربية: Sunovels" + - "中文, 汉语, 漢語: 69书吧" + - "中文, 汉语, 漢語: 爱下电子书" + - "中文, 汉语, 漢語: linovel" + - "中文, 汉语, 漢語: Linovelib" + - "中文, 汉语, 漢語: Linovelib(繁體)" + - "中文, 汉语, 漢語: Novel543" + - "中文, 汉语, 漢語: Quanben" + - "English: FirstKissNovel" + - "English: AllNovel" + - "English: AllNovelFull" + - "English: Arcane Translations" + - "English: Archive Of Our Own" + - "English: Srank Manga" + - "English: Belle Reservoir" + - "English: BoxNovel" + - "English: Cherry Mist Cafe" + - "English: Chrysanthemum Garden" + - "English: Citrus Aurora" + - "English: Coral Boutique" + - "English: CPUnovel" + - "English: Crimson Scrolls" + - "English: Daoist Quest" + - "English: DaoNovel" + - "English: DaoTranslate" + - "English: Divine Dao Library" + - "English: Dearest Rosalie" + - "English: Dragonholic" + - "English: Dragon Tea" + - "English: Dream Big Translations" + - "English: Dusk Blossoms" + - "English: ElloTL" + - "English: Eternalune" + - "English: Etude Translations" + - "English: FanNovel" + - "English: Fans Translations" + - "English: Fenrir Realm" + - "English: Fiction Zone" + - "English: Foxaholic" + - "English: Foxteller" + - "English: Faq Wiki" + - "English: Free Web Novel" + - "English: Galaxy Translations" + - "English: Genesis" + - "English: Guavaread" + - "English: Hiraeth Translation" + - "English: HotNovelPub" + - "English: Indra Translations" + - "English: Inkitt" + - "English: iNovelTranslation" + - "English: Ippotranslations" + - "English: KDT Novels" + - "English: Keopi Translations" + - "English: KnoxT" + - "English: Lazy Girl Translations" + - "English: LeafStudio" + - "English: Lib Read" + - "English: LightNovelCave" + - "English: LightNovelPlus" + - "English: LightNovelPub Vip" + - "English: Light Novel Translations" + - "English: Light Novel Updates" + - "English: Lily on the Valley" + - "English: LightNovelHeaven" + - "English: LnMTL" + - "English: Ltnovel" + - "English: LulloBox" + - "English: LunarLetters" + - "English: Meownovel" + - "English: Moonlight Novels" + - "English: MostNovel" + - "English: MTL-Novel" + - "English: MTL Novel" + - "English: MVLEMPYR" + - "English: MysticalSeries" + - "English: NeoSekai Translations" + - "English: Nitro Manga" + - "English: novelsOnline" + - "English: NobleMTL" + - "English: Noice Translations" + - "English: Novel Bin" + - "English: NovelBuddy" + - "English: NovelCool" + - "English: Novel Fire" + - "English: NovelFull" + - "English: Novel Hall" + - "English: NovelHi" + - "English: NovelLib" + - "English: Novelight" + - "English: NovelMultiverse" + - "English: Novel Ninja" + - "English: NovelRest" + - "English: NovelsKnight" + - "English: NovelTranslate" + - "English: Novel Updates" + - "English: Panda Machine Translations" + - "English: Pastel Tales" + - "English: PawRead" + - "English: Penguin Squad" + - "English: Prizma" + - "English: Rainofsnow" + - "English: Ranobes" + - "English: Ranovel" + - "English: Read Fanfic" + - "English: Read From Net" + - "English: ReadNovelFull" + - "English: Re:Library" + - "English: Requiem Translations" + - "English: Royal Road" + - "English: Salmon Latte" + - "English: Scribble Hub" + - "English: SleepyTranslations" + - "English: SonicMTL" + - "English: StorySeedling" + - "English: Sweet Escape" + - "English: System Translation" + - "English: TranslatinOtaku" + - "English: Translation Weaver" + - "English: Universal Novel" + - "English: Vandy Translate" + - "English: Violet Lily" + - "English: VyNovel" + - "English: Webnovel" + - "English: WebNovelLover" + - "English: Web Novel Translation" + - "English: Web Novel Pub" + - "English: White Moonlight Novels" + - "English: Witch Cult Translations" + - "English: Wook's Teahouse" + - "English: WordExcerpt" + - "English: WTR-LAB" + - "English: Wuxiafox" + - "English: Fans MTL" + - "English: Wuxiabox" + - "English: Wuxia Space" + - "English: WuxiaV" + - "English: Wuxia World" + - "English: WuxiaWorld.Site" + - "English: Zetro Translation" + - "Français: Chireads" + - "Français: HarkenEliwood" + - "Français: KissWood" + - "Français: Ligh Novel FR" + - "Français: MassNovel" + - "Français: MTL Novel (FR)" + - "Français: NovelDeGlace" + - "Français: Novhell" + - "Français: Warrior Legend Trad" + - "Français: WorldNovel" + - "Français: WuxiaLnScantrad" + - "Français: Xiaowaz" + - "Bahasa Indonesia: Baca Light Novel" + - "Bahasa Indonesia: IndoWebNovel" + - "Bahasa Indonesia: MeioNovel" + - "Bahasa Indonesia: Vanovel" + - "Bahasa Indonesia: MTL Novel (ID)" + - "Bahasa Indonesia: NovelBookID" + - "Bahasa Indonesia: SakuraNovel" + - "Bahasa Indonesia: Sekte Novel" + - "Bahasa Indonesia: WBNovel" + - "日本語: kakuyomu" + - "日本語: Syosetu" + - "조선말, 한국어: Agitoon" + - "조선말, 한국어: Fortune Eternal" + - "Polski: Novelki" + - "Português: Better Novels" + - "Português: Blog do Amon Novels" + - "Português: Central Novel" + - "Português: Illusia" + - "Português: Kiniga" + - "Português: LaNovels" + - "Português: Light Novel Brasil" + - "Português: MTL Novel (PT)" + - "Português: Novel Mania" + - "Português: Tsundoku Traduções" + - "Русский: Автор Тудей" + - "Русский: Bllate (API)" + - "Русский: Bookhamster" + - "Русский: Bookriver" + - "Русский: Erolate (API)" + - "Русский: EzNovels" + - "Русский: ficbook" + - "Русский: Свободный Мир Ранобэ" + - "Русский: Jaomix" + - "Русский: MTL Novel (RU)" + - "Русский: Neobook" + - "Русский: NovelCool (RU)" + - "Русский: Ranobes (RU)" + - "Русский: Renovels" + - "Русский: RanobeLib" + - "Русский: RanobeHub" + - "Русский: РанобэРФ" + - "Русский: Rulate (API)" + - "Русский: NovelTL" + - "Русский: ТопЛиба" + - "Русский: Целлюлоза" + - "Español: AllNovelRead" + - "Español: AnimesHoy12" + - "Español: Hasu Translations" + - "Español: LightNovelDaily" + - "Español: MTL Novel (ES)" + - "Español: NOVA" + - "Español: Novelas Ligera" + - "Español: Novelyra" + - "Español: Oasis Translations" + - "Español: Pancho Translations" + - "Español: ReinoWuxia" + - "Español: SkyNovels" + - "Español: TC & Sega" + - "Español: Traducciones Amistosas" + - "Español: TuNovelaLigera" + - "Español: Yuuki Tls" + - "ไทย: Novel Lucky" + - "ไทย: Novel PDF" + - "Türkçe: Araz Novel" + - "Türkçe: E-KİTAPLAR" + - "Türkçe: EpikNovel" + - "Türkçe: kakikata" + - "Türkçe: Kodeks Library" + - "Türkçe: MangaTR" + - "Türkçe: NABİ SCANS" + - "Türkçe: Namevt" + - "Türkçe: Novel oku" + - "Türkçe: NovelTR" + - "Türkçe: Ragnar Scans" + - "Türkçe: ThNovels" + - "Türkçe: TurkceLightNovels" + - "Türkçe: WebNovelOku" + - "Українська: BakaInUA" + - "Українська: Смаколики" + - "Tiếng Việt: Light Novel VN" + - "Tiếng Việt: Hako" + - "Tiếng Việt: Nettruyen" + - "Tiếng Việt: TruyenSS" + - "Multi: Komga" + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: Severity + description: Select the issue severity level. + multiple: false + options: + - Wrong Formatting + - Wrong Content + - Missing Chapter + - Missing Images + - Can't Load Novels + - Domain Changed + - Other + validations: + required: true + + - type: textarea + id: reproduce-steps + attributes: + label: Steps To Reproduce + description: Describe the steps to reproduce the issue. + placeholder: | + Example: + 1. Open the plugin + 2. Search for a novel + 3. Error appears + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What should happen? + placeholder: | + Example: + "The search results should display correctly..." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: | + Example: + "An error message appeared instead..." + validations: + required: true + + - type: input + id: lnreader-version + attributes: + label: LNReader Version + description: Find this in **More → About**. + placeholder: | + Example: "2.0.0" + validations: + required: true + + - type: input + id: plugin-version + attributes: + label: Plugin Version + description: Find this in **Browse → Installed** next to the plugin name. + placeholder: | + Example: "1.0.1" + validations: + required: true + + - type: input + id: android-version + attributes: + label: Android Version + description: Find this in your device settings. + placeholder: | + Example: "Android 15" + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other Details + description: Any additional information or screenshots. + placeholder: | + Additional context, screenshots, or relevant information. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if these requirements are not met. + options: + - label: I have searched existing issues and confirmed this is not a duplicate. + required: true + - label: I have written a clear and descriptive title. + required: true + - label: I have updated the app to the latest version **[2.0.0](https://github.com/LNReader/lnreader/releases/tag/v2.0.0-beta.3)**. + required: true + - label: I have updated all installed plugins to the latest version. + required: true + - label: I understand that app-specific issues should be reported in the [app repository](https://github.com/LNReader/lnreader/issues/new/choose). + required: true diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml new file mode 100644 index 000000000..b240ef4d9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -0,0 +1,39 @@ +name: Request Feature +description: Suggest an improvement for an existing plugin +labels: [Feature Request] +body: + - type: textarea + id: feature-description + attributes: + label: Feature Description + description: Describe the feature you would like to see added. + placeholder: | + Example: + "Add support for latest novels list in Royal Road plugin..." + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other Details + description: Any additional context or mockups. + placeholder: | + Additional information, use cases, or examples. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if these requirements are not met. + options: + - label: I have searched existing issues and confirmed this is not a duplicate. + required: true + - label: I have written a clear and descriptive title. + required: true + - label: I understand that app-specific features should be requested in the [app repository](https://github.com/LNReader/lnreader/issues/new/choose). + required: true + - label: I have updated the app to the latest version **[1.1.19](https://github.com/LNReader/lnreader/releases/latest)**. + required: true + - label: I have provided all requested information in this form. + required: true diff --git a/.github/ISSUE_TEMPLATE/request_plugin.yml b/.github/ISSUE_TEMPLATE/request_plugin.yml new file mode 100644 index 000000000..4c33f7521 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/request_plugin.yml @@ -0,0 +1,56 @@ +name: Request Plugin +description: Request a new plugin for LNReader +labels: [Plugin Request] +body: + - type: input + id: name + attributes: + label: Plugin Name + description: The name of the website or platform. + placeholder: | + Example: "Novel Updates" + validations: + required: true + + - type: input + id: link + attributes: + label: Website URL + description: The URL of the website. + placeholder: | + Example: "https://example.com" + validations: + required: true + + - type: input + id: language + attributes: + label: Language + description: The primary language of the website. + placeholder: | + Example: "English" + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other Details + description: Any additional information about the website. + placeholder: | + Why this plugin would be useful, special features, etc. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if these requirements are not met. + options: + - label: I have searched existing issues and confirmed this plugin has not been requested before. + required: true + - label: I have used the plugin name as the issue title. + required: true + - label: I have verified this plugin does not already exist in the [repository](https://github.com/LNReader/lnreader-plugins). + required: true + - label: I have provided all requested information in this form. + required: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..7905262e6 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,5 @@ +#### Checklist + +- [ ] Update version code if an existing plugin was modified +- [ ] Test changes in Plugin Playground or the app +- [ ] Reference related issues in the PR body (e.g. Closes #xyz) diff --git a/.github/scripts/blank_report_issue.yml b/.github/scripts/blank_report_issue.yml new file mode 100644 index 000000000..eae8ef0cf --- /dev/null +++ b/.github/scripts/blank_report_issue.yml @@ -0,0 +1,125 @@ +# DO NOT EDIT THIS FILE DIRECTLY +# This file is auto-generated from blank_report_issue.yml in .github/scripts/ +# Any changes made here will be overwritten when plugins are published. + +name: Report Issue +description: Report a plugin issue in LNReader +labels: [Bug] +body: + - type: dropdown + id: source + attributes: + label: Plugin + description: Select the affected plugin(s). + multiple: true + options: + {#CHANGE#} + validations: + required: true + + - type: dropdown + id: severity + attributes: + label: Severity + description: Select the issue severity level. + multiple: false + options: + - Wrong Formatting + - Wrong Content + - Missing Chapter + - Missing Images + - Can't Load Novels + - Domain Changed + - Other + validations: + required: true + + - type: textarea + id: reproduce-steps + attributes: + label: Steps To Reproduce + description: Describe the steps to reproduce the issue. + placeholder: | + Example: + 1. Open the plugin + 2. Search for a novel + 3. Error appears + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What should happen? + placeholder: | + Example: + "The search results should display correctly..." + validations: + required: true + + - type: textarea + id: actual-behavior + attributes: + label: Actual Behavior + description: What actually happened? + placeholder: | + Example: + "An error message appeared instead..." + validations: + required: true + + - type: input + id: lnreader-version + attributes: + label: LNReader Version + description: Find this in **More → About**. + placeholder: | + Example: "2.0.0" + validations: + required: true + + - type: input + id: plugin-version + attributes: + label: Plugin Version + description: Find this in **Browse → Installed** next to the plugin name. + placeholder: | + Example: "1.0.1" + validations: + required: true + + - type: input + id: android-version + attributes: + label: Android Version + description: Find this in your device settings. + placeholder: | + Example: "Android 15" + validations: + required: true + + - type: textarea + id: other-details + attributes: + label: Other Details + description: Any additional information or screenshots. + placeholder: | + Additional context, screenshots, or relevant information. + + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your issue will be closed if these requirements are not met. + options: + - label: I have searched existing issues and confirmed this is not a duplicate. + required: true + - label: I have written a clear and descriptive title. + required: true + - label: I have updated the app to the latest version **[2.0.0](https://github.com/LNReader/lnreader/releases/tag/v2.0.0-beta.3)**. + required: true + - label: I have updated all installed plugins to the latest version. + required: true + - label: I understand that app-specific issues should be reported in the [app repository](https://github.com/LNReader/lnreader/issues/new/choose). + required: true diff --git a/.github/scripts/generate-issue-template-options.cjs b/.github/scripts/generate-issue-template-options.cjs new file mode 100644 index 000000000..41b3f2da3 --- /dev/null +++ b/.github/scripts/generate-issue-template-options.cjs @@ -0,0 +1,69 @@ +const version = require('../../package.json').version; +const dist = `plugins/v${version}`; +const fs = require('fs'); + +const rawText = fs.readFileSync( + '.github/scripts/blank_report_issue.yml', + 'utf8', +); + +async function main() { + console.log(`Fetching plugins from: ${dist}`); + + try { + const response = await fetch( + `https://raw.githubusercontent.com/LNReader/lnreader-plugins/${dist}/.dist/plugins.min.json`, + ); + + if (!response.ok) { + throw new Error( + `Failed to fetch plugins: ${response.status} ${response.statusText}`, + ); + } + + const pluginsRaw = await response.json(); + console.log(`Found ${pluginsRaw.length} plugins`); + + const plugins = pluginsRaw.reduce((arr, plugin) => { + arr[plugin.lang + ': ' + plugin.name] = plugin.id; + return arr; + }, {}); + + let newKeys = Object.keys(plugins); + let savedKeys = []; + try { + let keys = JSON.parse( + fs.readFileSync('.github/scripts/keys.json', 'utf8'), + ); + savedKeys = Object.keys(keys); + console.log(`Loaded ${savedKeys.length} existing plugin keys`); + } catch (err) { + console.log('No existing keys file found, creating new one'); + } + + if (!sameKeys(newKeys, savedKeys) && Array.isArray(newKeys)) { + console.log('Plugin list has changed, updating issue template...'); + const text = newKeys.join('"\n - "'); + fs.writeFileSync( + '.github/ISSUE_TEMPLATE/report_issue.yml', + rawText.replace(/{#CHANGE#}/g, '- "' + text + '"'), + ); + fs.writeFileSync('.github/scripts/keys.json', JSON.stringify(plugins)); + console.log('Issue template updated successfully'); + } else { + console.log('No changes detected in plugin list'); + } + + function sameKeys(a, b) { + return a.length === b.length && a.every(value => b.includes(value)); + } + } catch (error) { + console.error('Error generating issue template options:', error.message); + throw error; + } +} + +main().catch(err => { + console.error('Fatal error:', err); + process.exit(1); +}); diff --git a/.github/scripts/keys.json b/.github/scripts/keys.json new file mode 100644 index 000000000..547ac2d51 --- /dev/null +++ b/.github/scripts/keys.json @@ -0,0 +1 @@ +{"‎العربية: ArNovel":"arnovel","‎العربية: Azora":"azora","‎العربية: dilar tube":"dilartube","‎العربية: Free Kol Novel":"freekolnovel","‎العربية: HizoManga":"hizomanga","‎العربية: Kol Novel":"kolnovel","‎العربية: Markazriwayat":"markazriwayat","‎العربية: Novel4Up":"novel4up","‎العربية: Novels Paradise":"novelsparadise","‎العربية: Olaoe.cyou":"olaoe","‎العربية: Rewayat Club":"rewayatclub","‎العربية: Riwyat":"riwyat","‎العربية: Sunovels":"sunovels","中文, 汉语, 漢語: 69书吧":"69shu","中文, 汉语, 漢語: 爱下电子书":"ixdzs8","中文, 汉语, 漢語: linovel":"linovel","中文, 汉语, 漢語: Linovelib":"linovelib","中文, 汉语, 漢語: Linovelib(繁體)":"linovelib_tw","中文, 汉语, 漢語: Novel543":"novel543","中文, 汉语, 漢語: Quanben":"quanben","English: FirstKissNovel":"1stkissnovel","English: AllNovel":"allnovel","English: AllNovelFull":"anf.net","English: Arcane Translations":"arcane","English: Archive Of Our Own":"archiveofourown","English: Srank Manga":"asuralightnovel","English: Belle Reservoir":"bellereservoir","English: BoxNovel":"boxnovel","English: Cherry Mist Cafe":"cherrymistcafe","English: Chrysanthemum Garden":"chrysanthemumgarden","English: Citrus Aurora":"citrusaurora","English: Coral Boutique":"coralboutique","English: CPUnovel":"cpunovel","English: Crimson Scrolls":"crimsonscrolls","English: Daoist Quest":"daoistquest","English: DaoNovel":"daonovel","English: DaoTranslate":"daotranslate","English: Divine Dao Library":"DDL.com","English: Dearest Rosalie":"dearestrosalie","English: Dragonholic":"dragonholic","English: Dragon Tea":"dragontea","English: Dream Big Translations":"dreambigtl","English: Dusk Blossoms":"duskblossoms","English: ElloTL":"ellotl","English: Eternalune":"eternalune","English: Etude Translations":"etudetranslations","English: FanNovel":"fannovel","English: Fans Translations":"fanstranslations","English: Fenrir Realm":"fenrir","English: Fiction Zone":"fictionzone","English: Foxaholic":"foxaholic","English: Foxteller":"foxteller","English: Faq Wiki":"FWK.US","English: Free Web Novel":"FWN.com","English: Galaxy Translations":"galaxytranslations","English: Genesis":"genesistudio","English: Guavaread":"guavaread","English: Hiraeth Translation":"hiraethtranslation","English: HotNovelPub":"hotnovelpub","English: Indra Translations":"indratranslations","English: Inkitt":"inkitt","English: iNovelTranslation":"inoveltranslation","English: Ippotranslations":"ippotranslations","English: KDT Novels":"kdtnovels","English: Keopi Translations":"keopi","English: KnoxT":"knoxt","English: Lazy Girl Translations":"lazygirltranslations","English: LeafStudio":"LeafStudio","English: Lib Read":"libread","English: LightNovelCave":"lightnovelcave","English: LightNovelPlus":"lightnovelplus","English: LightNovelPub Vip":"lightnovelpubvip","English: Light Novel Translations":"lightnoveltranslations","English: Light Novel Updates":"LightNovelUpdates","English: Lily on the Valley":"lilyonthevalley","English: LightNovelHeaven":"lnheaven","English: LnMTL":"lnmtl","English: Ltnovel":"ltnovel","English: LulloBox":"lullobox","English: LunarLetters":"lunarletters","English: Meownovel":"meownovel","English: Moonlight Novels":"moonlightnovel","English: MostNovel":"mostnovel","English: MTL-Novel":"mtl-novel","English: MTL Novel":"mtlnovel","English: MVLEMPYR":"mvlempyr.com","English: MysticalSeries":"mysticalmerries","English: NeoSekai Translations":"neosekaiTLS","English: Nitro Manga":"nitromanga","English: novelsOnline":"NO.net","English: NobleMTL":"noblemtl","English: Noice Translations":"noicetranslations","English: Novel Bin":"novelbin","English: NovelBuddy":"novelbuddy","English: NovelCool":"novelcool","English: Novel Fire":"novelfire","English: NovelFull":"novelfull","English: Novel Hall":"novelhall","English: NovelHi":"novelhi","English: NovelLib":"novelib","English: Novelight":"novelight","English: NovelMultiverse":"novelmultiverse","English: Novel Ninja":"novelninja","English: NovelRest":"novelrest","English: NovelsKnight":"novelsknight","English: NovelTranslate":"novelTL","English: Novel Updates":"novelupdates","English: Panda Machine Translations":"pandamtl","English: Pastel Tales":"pasteltales","English: PawRead":"pawread","English: Penguin Squad":"penguinsquad","English: Prizma":"prizmatranslation","English: Rainofsnow":"rainofsnow","English: Ranobes":"ranobes","English: Ranovel":"ranovel","English: Read Fanfic":"readfanfic","English: Read From Net":"readfrom","English: ReadNovelFull":"readnovelfull","English: Re:Library":"ReLib","English: Requiem Translations":"requiemtls","English: Royal Road":"royalroad","English: Salmon Latte":"salmonlatte","English: Scribble Hub":"scribblehub","English: SleepyTranslations":"sleeptTLS","English: SonicMTL":"sonicmtl","English: StorySeedling":"storyseedling","English: Sweet Escape":"sweetescape","English: System Translation":"systemtranslation","English: TranslatinOtaku":"translatinotaku","English: Translation Weaver":"transweaver","English: Universal Novel":"universalnovel","English: Vandy Translate":"vandytranslate","English: Violet Lily":"violetlily","English: VyNovel":"vynovel","English: Webnovel":"webnovel","English: WebNovelLover":"webnovelover","English: Web Novel Translation":"WebNovelTraslation","English: Web Novel Pub":"webnovelworld","English: White Moonlight Novels":"whitemoonlightnovels","English: Witch Cult Translations":"witchculttranslations","English: Wook's Teahouse":"wooksteahouse","English: WordExcerpt":"wordexcerpt","English: WTR-LAB":"WTRLAB","English: Wuxiafox":"wuxiacity","English: Fans MTL":"wuxiamtl","English: Wuxiabox":"wuxiap","English: Wuxia Space":"wuxiaspace","English: WuxiaV":"wuxiav","English: Wuxia World":"wuxiaworld","English: WuxiaWorld.Site":"wuxiaworld.site","English: Zetro Translation":"zetroTL","Français: Chireads":"chireads","Français: HarkenEliwood":"harkeneliwood","Français: KissWood":"kisswood","Français: Ligh Novel FR":"lightnovelfr","Français: MassNovel":"massnovel","Français: MTL Novel (FR)":"mtlnovel-fr","Français: NovelDeGlace":"noveldeglace","Français: Novhell":"novhell","Français: Warrior Legend Trad":"warriorlegendtrad","Français: WorldNovel":"worldnovel","Français: WuxiaLnScantrad":"wuxialnscantrad","Français: Xiaowaz":"xiaowaz","Bahasa Indonesia: Baca Light Novel":"bacalightnovel","Bahasa Indonesia: IndoWebNovel":"IDWN.id","Bahasa Indonesia: MeioNovel":"meionovel","Bahasa Indonesia: Vanovel":"morenovel","Bahasa Indonesia: MTL Novel (ID)":"mtlnovel-id","Bahasa Indonesia: NovelBookID":"novelbookid","Bahasa Indonesia: SakuraNovel":"sakura.id","Bahasa Indonesia: Sekte Novel":"sektenovel","Bahasa Indonesia: WBNovel":"wbnovel","日本語: kakuyomu":"kakuyomu","日本語: Syosetu":"yomou.syosetu","조선말, 한국어: Agitoon":"agit.xyz","조선말, 한국어: Fortune Eternal":"fortuneeternal","Polski: Novelki":"novelki.pl","Português: Better Novels":"betternovels","Português: Blog do Amon Novels":"blogdoamonnovels","Português: Central Novel":"centralnovel","Português: Illusia":"illusia","Português: Kiniga":"kiniga","Português: LaNovels":"lanovels","Português: Light Novel Brasil":"lightnovelbrasil","Português: MTL Novel (PT)":"mtlnovel-pt","Português: Novel Mania":"novelmania.com.br","Português: Tsundoku Traduções":"tsundoku","Русский: Автор Тудей":"AT","Русский: Bllate (API)":"bllate-api","Русский: Bookhamster":"bookhamster","Русский: Bookriver":"bookriver","Русский: Erolate (API)":"erolate-api","Русский: EzNovels":"eznovels","Русский: ficbook":"ficbook","Русский: Свободный Мир Ранобэ":"ifreedom","Русский: Jaomix":"jaomix.ru","Русский: MTL Novel (RU)":"mtlnovel-ru","Русский: Neobook":"neobook","Русский: NovelCool (RU)":"novelcool-ru","Русский: Ranobes (RU)":"ranobes-ru","Русский: Renovels":"ReN","Русский: RanobeLib":"RLIB","Русский: RanobeHub":"RNBH.org","Русский: РанобэРФ":"RNRF","Русский: Rulate (API)":"rulate-api","Русский: NovelTL":"TL","Русский: ТопЛиба":"TopLiba","Русский: Целлюлоза":"zelluloza","Español: AllNovelRead":"allnovelread","Español: AnimesHoy12":"AnimesHoy12","Español: Hasu Translations":"HasuTL","Español: LightNovelDaily":"lightnoveldaily","Español: MTL Novel (ES)":"mtlnovel-es","Español: NOVA":"nova","Español: Novelas Ligera":"novelasligera","Español: Novelyra":"novelyra","Español: Oasis Translations":"oasistranslations","Español: Pancho Translations":"panchotranslations","Español: ReinoWuxia":"reinowuxia","Español: SkyNovels":"skynovels","Español: TC & Sega":"TCSega","Español: Traducciones Amistosas":"traducciones","Español: TuNovelaLigera":"tunovelaligera","Español: Yuuki Tls":"yuukitls","ไทย: Novel Lucky":"novel-lucky","ไทย: Novel PDF":"novelpdf","Türkçe: Araz Novel":"azraznovel","Türkçe: E-KİTAPLAR":"ekitaplar","Türkçe: EpikNovel":"epiknovel","Türkçe: kakikata":"Kakikata","Türkçe: Kodeks Library":"kodekslibrary","Türkçe: MangaTR":"mangatr","Türkçe: NABİ SCANS":"nabiscans","Türkçe: Namevt":"namevt","Türkçe: Novel oku":"noveloku","Türkçe: NovelTR":"noveltr","Türkçe: Ragnar Scans":"ragnarscans","Türkçe: ThNovels":"thnovels","Türkçe: TurkceLightNovels":"turkcelightnovels","Türkçe: WebNovelOku":"webnoveloku","Українська: BakaInUA":"bakainua","Українська: Смаколики":"smakolykytl","Tiếng Việt: Light Novel VN":"lightnovel.vn","Tiếng Việt: Hako":"ln.hako","Tiếng Việt: Nettruyen":"nettruyen","Tiếng Việt: TruyenSS":"truyenss.com","Multi: Komga":"komga"} \ No newline at end of file diff --git a/.github/workflows/issue_auto_label.yml b/.github/workflows/issue_auto_label.yml new file mode 100644 index 000000000..efd88ab9e --- /dev/null +++ b/.github/workflows/issue_auto_label.yml @@ -0,0 +1,80 @@ +name: Auto Label Issues + +on: + issues: + types: + - opened + - edited + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + auto-label: + name: Auto Label Issues + if: ${{ github.event.issue.body && contains(github.event.issue.body, '### Plugin') && contains(github.event.issue.labels.*.name, 'Bug') }} + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Extract Severity From Issue Body + id: extract-severity + run: | + USER_SELECTED_SEVERITY=$(echo "${{ github.event.issue.body }}" | grep "### Severity" -A 2 | tail -n 1 | sed 's/[[:space:]]*$//') + SELECTED_SEVERITY="" + + case "$USER_SELECTED_SEVERITY" in + "Wrong Formatting"|"Wrong Content"|"Missing Chapter"|"Missing Images"|"Can't Load Novels"|"Domain Changed") + SELECTED_SEVERITY="$USER_SELECTED_SEVERITY" + ;; + *) + SELECTED_SEVERITY="Other" + ;; + esac + echo "SELECTED_SEVERITY=$SELECTED_SEVERITY" >> $GITHUB_ENV + + - name: Extract Plugin From Issue Body + id: parse-issue + run: | + SELECTED_PLUGIN=$(echo "${{ github.event.issue.body }}" | awk '/### Plugin/ {f=1; next} f && NF {print; exit}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + echo "SELECTED_PLUGIN=$SELECTED_PLUGIN" >> $GITHUB_ENV + + - name: Extract Language From Issue Body + id: extract-language + run: | + SELECTED_LANGUAGE=$(echo "${{ github.event.issue.body }}" | awk '/### Language/ {f=1; next} f && NF {print; exit}' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') + + echo "SELECTED_LANGUAGE=$SELECTED_LANGUAGE" >> $GITHUB_ENV + + - name: Determine Corresponding Label + id: determine-label + run: | + LABELS="$( jq --arg keys "${{ env.SELECTED_PLUGIN }}" '. as $data | $keys | split(", ") as $keyList | $keyList[] | . as $key | if $data[$key] != null then "\($key)[\($data[$key])]" else empty end' ./.github/scripts/keys.json | sed 's/"//g' | sed 's/^[^:]*: //' )" + + # Add "Plugin: " in front of each label + LABELS_WITH_PREFIX="$(echo "$LABELS" | sed 's/^/Plugin: /')" + + FINAL_LABELS="$LABELS_WITH_PREFIX" + + if [ -n "$SELECTED_LANGUAGE" ]; then + FINAL_LABELS="${FINAL_LABELS}"$'\n'"Language: $SELECTED_LANGUAGE" + fi + + # Save to GitHub environment + printf "LABELS<> $GITHUB_ENV + + - name: Add Labels To Issue + if: env.LABELS != '' + uses: actions-ecosystem/action-add-labels@v1 + with: + labels: | + Severity: ${{ env.SELECTED_SEVERITY }} + ${{ env.LABELS }} + + - name: Handle Missing Label + if: env.LABELS == '' + run: echo "No matching label found for the selected plugin." diff --git a/.github/workflows/issue_moderator.yml b/.github/workflows/issue_moderator.yml new file mode 100644 index 000000000..481bc770f --- /dev/null +++ b/.github/workflows/issue_moderator.yml @@ -0,0 +1,48 @@ +name: Issue Moderator + +on: + issues: + types: [opened, edited, reopened] + issue_comment: + types: [created] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: false + +jobs: + autoclose: + name: Auto Close Invalid Issues + runs-on: ubuntu-latest + steps: + - name: Moderate Issues + uses: tachiyomiorg/issue-moderator-action@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + duplicate-label: Duplicate + + duplicate-check-enabled: true + duplicate-check-labels: | + ["Plugin Request", "Domain Changed"] + + existing-check-enabled: false + + auto-close-rules: | + [ + { + "type": "body", + "regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*", + "message": "The acknowledgment section was not removed." + }, + { + "type": "body", + "regex": ".*\\* (LNReader version|Android version|Device): \\?.*", + "message": "Requested information in the template was not filled out." + }, + { + "type": "title", + "regex": ".*(Plugin name|Short description).*", + "message": "You did not fill out the description in the title" + } + ] + auto-close-ignore-label: do-not-autoclose diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 000000000..33cd3350d --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,22 @@ +name: Lock Threads + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + lock: + name: Lock Inactive Threads + runs-on: ubuntu-latest + steps: + - name: Lock Inactive Issues And Pull Requests + uses: dessant/lock-threads@v5 + with: + github-token: ${{ github.token }} + issue-lock-inactive-days: '2' + pr-lock-inactive-days: '2' diff --git a/.github/workflows/prettier.yml b/.github/workflows/prettier.yml new file mode 100644 index 000000000..93fb35a07 --- /dev/null +++ b/.github/workflows/prettier.yml @@ -0,0 +1,33 @@ +name: Format Check + +on: + pull_request: + branches: [master] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format-check: + name: Check Code Formatting + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Prettier + run: | + rm package.json + rm package-lock.json + npm init -y + npm install prettier@3.2.5 + + - name: Check Code Formatting + run: npx prettier --check "./src/**/*.{ts,tsx,js,css}" diff --git a/.github/workflows/publish-plugins.yml b/.github/workflows/publish-plugins.yml new file mode 100644 index 000000000..490bf2ab1 --- /dev/null +++ b/.github/workflows/publish-plugins.yml @@ -0,0 +1,48 @@ +name: Publish Plugins + +on: + push: + branches: + - master + paths: + - 'plugins/**' + - 'public/**' + - 'scripts/publish-plugins.sh' + - '.github/workflows/publish-plugins.yml' + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + publish: + name: Publish Plugins + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.REPO_SCOPED_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Dependencies + run: npm install --omit=dev --ignore-scripts + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + + - name: Publish Plugins (Main Repository) + if: github.repository == 'LNReader/lnreader-plugins' + run: npm run publish:plugins 2>> $GITHUB_STEP_SUMMARY + shell: bash + + - name: Publish Plugins (Fork - All Branches) + if: github.repository != 'LNReader/lnreader-plugins' + run: npm run publish:plugins -- --all-branches + shell: bash diff --git a/.github/workflows/theme_auto_label.yml b/.github/workflows/theme_auto_label.yml new file mode 100644 index 000000000..daf32035d --- /dev/null +++ b/.github/workflows/theme_auto_label.yml @@ -0,0 +1,135 @@ +name: Auto Theme Labeler + +on: + issues: + types: [labeled] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + auto-label: + name: Auto Label Theme + if: contains(github.event.issue.labels.*.name, 'Plugin Request') + runs-on: ubuntu-latest + env: + REPO: ${{ github.repository }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + steps: + - name: Get Plugin Website URL + id: get_url + env: + ISSUE_BODY: ${{ github.event.issue.body }} + run: | + URL=$(echo "$ISSUE_BODY" | sed -n '/### Website URL/,+2p' | sed '1,2d' | tr -d '[:space:]') + echo "URL=$URL" >> $GITHUB_OUTPUT + echo "Using URL: \`$URL\`" >> $GITHUB_STEP_SUMMARY + + - name: Get Base URL + id: get_base_url + env: + URL: ${{ steps.get_url.outputs.URL }} + run: | + BASE_URL=$(echo "$URL" | sed 's/https\?:\/\///' | cut -d'/' -f1) + echo "BASE_URL=$BASE_URL" >> $GITHUB_OUTPUT + echo "Using Base URL: \`$BASE_URL\`" >> $GITHUB_STEP_SUMMARY + + - name: Get User Agent + id: get_user_agent + run: | + CHROME_VERSION=$(curl -s https://chromiumdash.appspot.com/fetch_releases?channel=Stable\&platform=Windows\&num=1 | jq -r '.[0].version // "126.0.6478.36"') + echo "USER_AGENT=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/$CHROME_VERSION Safari/537.36" >> $GITHUB_OUTPUT + + - name: Check Madara Theme + id: check_madara + env: + BASE_URL: ${{ steps.get_base_url.outputs.BASE_URL }} + USER_AGENT: ${{ steps.get_user_agent.outputs.USER_AGENT }} + run: | + echo "🔍 Checking for Madara theme..." >> $GITHUB_STEP_SUMMARY + if curl --location --output /dev/null --silent --head --fail --max-time 10 "https://$BASE_URL/wp-content/themes/madara/style.css" -H "User-Agent: $USER_AGENT" 2>/dev/null || \ + curl --location --output /dev/null --silent --head --fail --max-time 10 "http://$BASE_URL/wp-content/themes/madara/style.css" -H "User-Agent: $USER_AGENT" 2>/dev/null; then + echo "madara=true" >> $GITHUB_OUTPUT + echo "✅ Madara Theme Detected" >> $GITHUB_STEP_SUMMARY + else + echo "madara=false" >> $GITHUB_OUTPUT + echo "❌ Madara Theme Not Found" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Lightnovelwp Theme + id: check_lighnovelwp + if: always() && steps.check_madara.outputs.madara != 'true' + env: + BASE_URL: ${{ steps.get_base_url.outputs.BASE_URL }} + USER_AGENT: ${{ steps.get_user_agent.outputs.USER_AGENT }} + run: | + echo "🔍 Checking for Lightnovelwp theme..." >> $GITHUB_STEP_SUMMARY + if curl --location --output /dev/null --silent --head --fail --max-time 10 "https://$BASE_URL/wp-content/themes/lightnovel/style.css" -H "User-Agent: $USER_AGENT" 2>/dev/null || \ + curl --location --output /dev/null --silent --head --fail --max-time 10 "http://$BASE_URL/wp-content/themes/lightnovel/style.css" -H "User-Agent: $USER_AGENT" 2>/dev/null; then + echo "lighnovelwp=true" >> $GITHUB_OUTPUT + echo "✅ Lightnovelwp Theme Detected" >> $GITHUB_STEP_SUMMARY + else + echo "lighnovelwp=false" >> $GITHUB_OUTPUT + echo "❌ Lightnovelwp Theme Not Found" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check HotNovelPub Theme + id: check_hotnovelpub + if: always() && steps.check_madara.outputs.madara != 'true' && steps.check_lighnovelwp.outputs.lighnovelwp != 'true' + env: + BASE_URL: ${{ steps.get_base_url.outputs.BASE_URL }} + USER_AGENT: ${{ steps.get_user_agent.outputs.USER_AGENT }} + run: | + echo "🔍 Checking for HotNovelPub theme..." >> $GITHUB_STEP_SUMMARY + if curl --location --silent --fail --max-time 10 "https://api.$BASE_URL" -H "User-Agent: $USER_AGENT" 2>/dev/null | grep -q "Hello\!" || \ + curl --location --silent --fail --max-time 10 "http://api.$BASE_URL" -H "User-Agent: $USER_AGENT" 2>/dev/null | grep -q "Hello\!"; then + echo "hotnovelpub=true" >> $GITHUB_OUTPUT + echo "✅ HotNovelPub Theme Detected" >> $GITHUB_STEP_SUMMARY + else + echo "hotnovelpub=false" >> $GITHUB_OUTPUT + echo "❌ HotNovelPub Theme Not Found" >> $GITHUB_STEP_SUMMARY + fi + + - name: Check Fictioneer Theme + id: check_fictioneer + if: always() && steps.check_madara.outputs.madara != 'true' && steps.check_lighnovelwp.outputs.lighnovelwp != 'true' && steps.check_hotnovelpub.outputs.hotnovelpub != 'true' + env: + BASE_URL: ${{ steps.get_base_url.outputs.BASE_URL }} + USER_AGENT: ${{ steps.get_user_agent.outputs.USER_AGENT }} + run: | + echo "🔍 Checking for Fictioneer theme..." >> $GITHUB_STEP_SUMMARY + if curl --location --output /dev/null --silent --head --fail --max-time 10 "https://$BASE_URL/wp-content/themes/fictioneer/css/application.css" -H "User-Agent: $USER_AGENT" 2>/dev/null || \ + curl --location --output /dev/null --silent --head --fail --max-time 10 "http://$BASE_URL/wp-content/themes/fictioneer/css/application.css" -H "User-Agent: $USER_AGENT" 2>/dev/null; then + echo "fictioneer=true" >> $GITHUB_OUTPUT + echo "✅ Fictioneer Theme Detected" >> $GITHUB_STEP_SUMMARY + else + echo "fictioneer=false" >> $GITHUB_OUTPUT + echo "❌ Fictioneer Theme Not Found" >> $GITHUB_STEP_SUMMARY + fi + + - name: Add Theme Label To Issue + if: always() + env: + MADARA: ${{ steps.check_madara.outputs.madara }} + LIGHNOVELWP: ${{ steps.check_lighnovelwp.outputs.lighnovelwp }} + HOTNOVELPUB: ${{ steps.check_hotnovelpub.outputs.hotnovelpub }} + FICTIONEER: ${{ steps.check_fictioneer.outputs.fictioneer }} + GH_TOKEN: ${{ github.token }} + run: | + echo "## Theme Detection Result" >> $GITHUB_STEP_SUMMARY + if [[ "$MADARA" == "true" ]]; then + echo "🏷️ Adding label: Theme: Madara" >> $GITHUB_STEP_SUMMARY + gh issue edit $ISSUE_NUMBER --add-label "Theme: Madara" -R $REPO + elif [[ "$LIGHNOVELWP" == "true" ]]; then + echo "🏷️ Adding label: Theme: Lighnovelwp" >> $GITHUB_STEP_SUMMARY + gh issue edit $ISSUE_NUMBER --add-label "Theme: Lighnovelwp" -R $REPO + elif [[ "$HOTNOVELPUB" == "true" ]]; then + echo "🏷️ Adding label: Theme: Hotnovelpub" >> $GITHUB_STEP_SUMMARY + gh issue edit $ISSUE_NUMBER --add-label "Theme: Hotnovelpub" -R $REPO + elif [[ "$FICTIONEER" == "true" ]]; then + echo "🏷️ Adding label: Theme: Fictioneer" >> $GITHUB_STEP_SUMMARY + gh issue edit $ISSUE_NUMBER --add-label "Theme: Fictioneer" -R $REPO + else + echo "❌ No Theme Detected" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.github/workflows/update-issue-template.yml b/.github/workflows/update-issue-template.yml new file mode 100644 index 000000000..7be4afbcb --- /dev/null +++ b/.github/workflows/update-issue-template.yml @@ -0,0 +1,53 @@ +name: Update Issue Template + +on: + workflow_run: + workflows: + - Publish Plugins + types: + - completed + branches: + - master + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + update_template: + name: Update Issue Template + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + if: | + github.repository == 'LNReader/lnreader-plugins' && + github.event.workflow_run.conclusion == 'success' && + github.event.workflow_run.event == 'push' + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: master + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Generate Issue Template Options + run: node ./.github/scripts/generate-issue-template-options.cjs + + - name: Create Or Update Pull Request + uses: peter-evans/create-pull-request@v7 + with: + commit-message: 'chore: Update Issue Template Plugin Options [skip ci]' + branch: 'update-issue-template' + title: 'chore: Update Issue Template Plugin Options [skip ci]' + body: | + Updates the issue template with the latest plugin dropdown options. + + This PR is automatically generated after plugins are published. + labels: 'bot' + base: 'master' + delete-branch: true diff --git a/.gitignore b/.gitignore index 6ffb8b674..99be8a8fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,18 @@ -/node_modules +node_modules +.vscode +.idea +.env +config.json -Procfile \ No newline at end of file +# generated multisrc .ts files +plugins/*/*\[*\]*.ts + +# ignore dist stuffs +.js +.dist +.DS_Store +dist +total.svg +src/plugins/index.ts +.history +broken-sites-report.json \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 000000000..31354ec13 --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 000000000..36af21989 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..0bdfd9d90 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules +.js +plugins/multisrc/fictioneer/custom diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 000000000..83254b77f --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,10 @@ +export default { + tabWidth: 2, + bracketSpacing: true, + useTabs: false, + bracketSameLine: false, + singleQuote: true, + trailingComma: 'all', + arrowParens: 'avoid', + quoteProps: 'preserve', +}; diff --git a/BLACKLIST.json b/BLACKLIST.json new file mode 100644 index 000000000..eadb03068 --- /dev/null +++ b/BLACKLIST.json @@ -0,0 +1,10 @@ +[ + { + "name": "Novel Oku", + "site": "https://novelokutr.net/", + "lang": "Turkish", + "reason": "Requested by Owner", + "date": "2025-06-28", + "aliases": ["novelokutr.net", "www.novelokutr.net"] + } +] diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..cfe50792f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Rajarshee Chatterjee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 000000000..02e3116fd --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# LNReader Plugins + +

+Total number of available plugins +Open plugin requests +Open bug reports +

+ +Community-driven plugin repository for [LNReader](https://github.com/LNReader/lnreader). This repository hosts plugins and manages related issues and requests. + +## Quick Start + +**Prerequisites:** Node.js >= 22 + +```bash +npm install +npm run dev:start +``` + +## Documentation + +- **[Quick Start Guide](./docs/quickstart.md)** - Create your first plugin +- **[Plugin Development](./docs/docs.md)** - Complete API reference +- **[Testing Guide](./docs/website-tutorial.md)** - Test plugins using the web interface +- **[Komga Plugin](./docs/komga-plugin.md)** - Self-hosted server integration + +## Testing Methods + +### Web Interface + +```bash +npm run dev:start +``` + +Open [localhost:3000](http://localhost:3000) to test plugins interactively. See the [testing guide](./docs/website-tutorial.md) for details. + +### Mobile App + +**From GitHub (Automated):** + +Push your changes to the `master` branch. The [GitHub Action](./.github/workflows/publish-plugins.yml) automatically builds and publishes plugins to the `plugins` branch. + +Add your repository URL to the app: + +``` +https://raw.githubusercontent.com///plugins//.dist/plugins.min.json +``` + +**From Localhost:** + +```bash +npm run serve:dev +``` + +Add `http://10.0.2.2/.dist/plugins.min.json` (Android emulator) to the app. Requires `.env` configuration (see `.env.template`). + +## Disclaimer + +The developers are not affiliated with any content providers. If you are a non-aggregator website owner, you may request plugin removal via [Discord](https://discord.gg/QdcWN4MD63) or by [creating an issue](https://github.com/LNReader/lnreader-plugins/issues/new). Removed sites are added to the [blacklist](BLACKLIST.json). diff --git a/components.json b/components.json new file mode 100644 index 000000000..2b0833f09 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/docs/docs.md b/docs/docs.md new file mode 100644 index 000000000..e21efff0b --- /dev/null +++ b/docs/docs.md @@ -0,0 +1,642 @@ +## Documentation for LNReader plugins + +- [PluginBase](#pluginbase) + - [NovelItem](#novelitem) + - [SourceNovel](#sourcenovel) + - [ChapterItem](#chapteritem) + - [Filters](#filters) + - [PluginSettings](#pluginsettings) +- [Using Cheerio](#using-cheerio) +- [Custom fetching functions](#custom-fetching-functions) + +Most of the Plugin/Novel type definitions accessed using the `Plugin` namespace imported via + +```ts +import { Plugin } from '@/types/plugin'; +``` + +### PluginBase + +PluginBase is a base class for all plugins. + +```ts +class ExamplePlugin implements Plugin.PluginBase {} +``` + +| Field | Required | Description | +| -------------------------------------------------------------- | -------- | ----------------------------------------------------- | +| [id](#pluginbaseid) | yes | Plugin ID | +| [name](#pluginbasename) | yes | Plugin Name | +| [icon](#pluginbasename) | yes | Plugin Icon | +| [site](#pluginbasesite) | yes | Plugin site link | +| [version](#pluginbaseversion) | yes | Plugin version | +| [imageRequestInit](#pluginbaseimagerequestinit) | no | Plugin Image Request Init | +| [filters](#pluginbasefilters) | no | [Filter definition](#filter-definition-object) object | +| [pluginSettings](#pluginbasepluginsettings) | no | [Plugin settings](#pluginsettings) object | +| [popularNovels(page, options)](#pluginbasepopularnovels) | yes | Novel list getter | +| [parseNovel(path)](#pluginbaseparsenovel) | yes | Novel info and chapter list getter | +| [parseChapter(path)](#pluginbaseparsechapter) | yes | Chapter text getter | +| [searchNovels(searchTerm, page)](#pluginbasesearchnovels) | yes | Novel searching getter | + +#### PluginBase::id + +Unique ID of your plugin + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + id = "templateID"; + ... +} +``` + +#### PluginBase::name + +The name of your plugin that is shown in-app + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + name = "template Plugin"; + ... +} +``` + +#### PluginBase::icon + +The path to your plugin's icon inside of `public/static` folder + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + icon = "src/en/templateplugin/icon.png"; + ... +} +``` + +> [!WARNING] +> Icons should be 96x96px + +#### PluginBase::site + +The url to the plugin's site + +###### Example + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + site = "https://example.com"; + ... +} +``` + +#### PluginBase::version + +Version of your plugin formatted according to [semver2.0 spec](https://semver.org/) i.e. `..` + +Where + +- `patch` increments on small fixes that fix the plugin (like site changed a selector, filter had a typo etc.) +- `minor` increments on fixes that improve the plugin (like adding/removing filters, adding search options etc.) +- `major` increments on fixes that fix the major issues with the plugin (like changing site link) + +###### Example + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + version = "1.0.0"; + ... +} +``` + +#### PluginBase::imageRequestInit + +The init for request to obtain images + +Used if images failed to load due to site's protection + +###### Example + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: 'https://example.com', + }, + }; + ... +} +``` + +#### PluginBase::filters + +A [Filter definition]() object that holds filters used in [popularNovels](#pluginbasepopularnovels) function + +###### Example + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + filters = { + order: { + label:"Order", + options: [ + { label: "Popular", value: "" }, + { label: "Newest", value: "newest" } + ], + type: FilterTypes.Picker, + value: "" + }, + status: { + label: "Status", + options: [ + { label: "All", value: "" }, + { label: "Ongoing", value: "ongoing" }, + { label: "Hiatus", value: "hiatus" }, + { label: "Completed", value: "completed" }, + ], + type: FilterTypes.Picker, + value: "", + } + } + ... +} +``` + +#### PluginBase::popularNovels + +Function that is used to get the (filtered) list of novels from the front page of the site + +```ts +async popularNovels( + page: number, + options: Plugin.PopularNovelsOptions + ): Promise +``` + +See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents + +###### Parameters + +- `page` current page to fetch +- `options` [PopularNovelsOptions](#pluginbasepopularnovelsoptions) + +###### Returns + +`NovelItem[]` An array of filtered main-page [NovelItems](#novelitem) + +###### Example: + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + async popularNovels( + page: number, + options: Plugin.PopularNovelsOptions + ): Promise { + const novels: Plugin.NovelItem[] = []; + if(options.filters.example.value === "test"){ + novels.push({ + name: "Novel1", + path: "/novel1", + cover:defaultCover + }) + } + return novels; + } +} +``` + +##### PluginBase::PopularNovelsOptions + +This type is used for getting the options of the [popularNovels](#pluginbasepopularnovels) function + +- `showLatestNovels: boolean` flag set when opened with `Latest` button + +- `filters: FilterValues` object containing all selected filter values. [More about Filters](#filters) + +#### PluginBase::parseNovel + +Function that is used to get the information about particular novel and the list of it's chapters + +```ts +async parseNovel(novelPath: string): Promise +``` + +See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents + +###### Parameters + +- `novelPath` value from [NovelItem::path](#novelitempath) + +###### Returns + +`SourceNovel` Novel information and chapter list as [SourceNovel](#sourcenovel) object + +> [!CAUTION] > [SourceNovel::path]() should be the same value as [NovelItem::path]() provided as parameter! + +###### Example: + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: "test", + artist: "none", + author: "none", + cover: defaultCover, + genres: "Isekai, Neverland", + status: NovelStatus.Completed, + summary: "" + }; + let chapters: Plugin.ChapterItem[] = []; + const chapter: Plugin.ChapterItem = { + name: "", + path: "", + releaseTime: "", + chapterNumber: 0, + }; + chapters.push(chapter); + novel.chapters = chapters; + return novel; + } + ... +} +``` + +#### PluginBase::parseChapter + +Function that is used to get the information about particular novel and the list of it's chapters + +```ts +async parseChapter(chapterPath: string): Promise +``` + +See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents + +###### Parameters + +- `chapterPath` value from [ChapterItem::path](#chapteritempath) + +###### Returns + +`string` HTML content of the chapter + +###### Example: + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + async parseChapter(chapterPath: string): Promise{ + return "

No chapter here

"; + } + ... +} +``` + +#### PluginBase::searchNovels + +Function that is used to find Novels in the source + +```ts +async searchNovels(searchTerm: string, pageNo: number): Promise +``` + +See [Using cheerio](#using-cheerio) for more information on how to parse HTML documents + +###### Parameters + +- `searchTerm` the search term +- `page` search page number + +###### Returns + +`NovelItem[]` An array of found [NovelItems](#novelitem) + +###### Example + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + async searchNovels( + searchTerm: string, + pageNo: number + ): Promise { + let novels: Plugin.NovelItem[] = []; + return novels; + } + ... +} +``` + +--- + +### NovelItem + +It is an object representing information how to store/access the novel + +| Field | type | Required | Description | +| -------------------------------- | -------- | -------- | ------------------------------------------ | +|

path

| `string` | yes | The relative path to the novel | +|

name

| `string` | yes | The name of the novel shown in the library | +|

cover

| `string` | no | URL to novel's cover | + +#### Default cover + +You can use the default `Cover not available` cover by importing + +```ts +import { defaultCover } from '@libs/defaultCover'; +``` + +--- + +### SourceNovel + +| Field | Type | Required | Desciption | +| ------- | ------------------------- | -------- | ---------- | +| path | string | yes | | +| name | string | no | string | +| cover | `string` | no | | +| genres | `string` | no | | +| summary | `string` | no | | +| author | `string` | no | | +| artist | `string` | no | | +| status | [NovelStatus] or `string` | no | | + + chapters?: ChapterItem[]; + +--- + +### ChapterItem + +| Field | Type | Required | Description | +| ------------- | -------- | -------- | ------------------------------- | +| name | string | yes | | +| path | string | yes | | +| releaseTime | string | no | release time in `YYYY-MM-DD` | +| chapterNumber | number | no | | +| page | string | no | for multi-page chapter lists | + +### Filters + +`Filters` and `FilterTypes` are not in the `Plugin` namespace and are from `@libs/filterInputs` file: + +```ts +import { FilterTypes, Filters } from '@libs/filterInputs'; +``` + +There are 2 main objects when using filters: + +- [Filter definition](#filter-definition-object) object +- [FilterValues](#filterValue) object + +#### Filter definition object + +This is the user-defined object that defines strictly what filters are available in the "filter" menu in app. +Every property of this object is a different filter. The key of the object is the name that will be used to reference this filter's value in the [FilterValues](#filtervalues-object) object + +```ts +filters = { + order: {} +} satisfies Filters; +// accessible in popularNovels as +options.filters.order +``` + +> [!CAUTION] +> Do not forget to add `satisfies Filters` after the Filter definition object! + +##### FilterProperties + +| Name | Type | Required | Desciption | +| ------- | ---------------------------- | ------------- | ------------------------------------------------------------------ | +| label | `string` | yes | in-app label | +| type | `FilterTypes` | yes | type of the filter | +| value | [check types](#filter-types) | yes | Default value for this filter and the starting filter state in-app | +| options | [check types](#filter-types) | in some types | The options available in the given type | + +###### Example + +```ts +filters = { + genre: { + type: FilterTypes.CheckboxGroup, + label: 'Genres', + value: [], + options: [ + { label: 'Isekai', value: 'isekai' }, + { label: 'Romance', value: 'romans' }, + ], + }, +} satisfies Filters; +``` + +##### Filter types + +Types of filters supported + +| FilterType | Description | `value` | `options` | +| ------------------------- | ------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ----------------------------------------------- | +| `Picker` | A spinner for choosing one of the choices provided in `options` | `string` the picked value | [Picker](#picker-options) options | +| `TextInput` | A filter allowing a free text input | `string` written value | N/A | +| `Switch` | A boolean switch | `boolean` state of the switch | N/A | +| `CheckboxGroup` | A grouping of checkboxes | `string[]` array containing selected values | [CheckboxGroup](#checkboxgroup-options) options | +| `ExcludableCheckboxGroup` | A filter allowing to pick one of the choices provided in `options` | [ExcludableCheckboxGroupValues](#excludablecheckboxgroupvalue-object) object | [CheckboxGroup](#checkboxgroup-options) options | + +###### Picker options + +```ts +options: [ + { + label: 'default', // in-app label + value: '', // in-code value + }, + { + label: 'Value ABC', + value: 'abc', + }, +]; +``` + +###### CheckboxGroup options + +```ts +options: [ + { + label: 'Value ABC', // in-app label + value: 'abc', // in-code value + }, + { + label: 'Value DEF', + value: 'def', + }, +]; +``` + +#### FilterValues object + +It is an object used inisde of `popularNovels` that contains selected values for all filters defined in the [Filter definition](#filter-definition-object) object. +The keys of the filter values correspond to Filter definition keys + +```ts +// Filter definition object +filters = { abc: {} } satisfies Filters; + +// then +options.filters; // FilterValues +options.filters.abc; // FilterValue for abc filter +``` + +##### FilterValue + +Properties of FilterValue: + +- `type: FilterType` type of the filter +- `value` value dependent on [FilterTypes](#filter-types) + +```ts +options.filters.abc.value; // value of the filter +options.filters.abc.type; // type of the filter +``` + +###### ExcludableCheckboxGroupValue object + +```ts +{ + included: string[], // options with selected selected + excluded: string[] // options with excluded selected +} +``` + +--- + +### PluginSettings + +Plugin settings allow plugins to define user-configurable options that are displayed in the app's settings UI. These settings are persistent and can be accessed within the plugin code. + +#### PluginBase::pluginSettings + +A user-defined object that defines configurable settings for the plugin. Each property of this object is a different setting that will be displayed in the app's settings UI. + +```ts +pluginSettings = { + settingKey: { + value: '', + label: 'Setting Label', + type: 'Text', // optional, defaults to 'Text' + }, +}; +``` + +##### Setting Properties + +| Name | Type | Required | Description | +| ------- | -------- | -------- | ---------------------------------------------- | +| value | `string` | yes | Default value for this setting | +| label | `string` | yes | Display label shown in the app's settings UI | +| type | `string` | no | Type of the setting UI component (see below) | + +##### Setting Types + +Currently, two setting types are supported: + +| Type | Description | UI Component | Default Value Type | +| -------- | ---------------------------------------------- | ------------ | ------------------ | +| `Switch` | A boolean toggle switch | SwitchItem | `boolean` | +| `Text` | A text input field (default if type is omitted) | TextInput | `string` | + +> [!NOTE] +> If `type` is not specified, the setting defaults to `Text` type and will be rendered as a TextInput. + +##### Accessing Settings Values + +Settings values are stored and can be accessed using the `storage` utility: + +```ts +import { storage } from '@libs/storage'; + +// Get a setting value +const settingValue = storage.get('settingKey'); + +// Set a setting value +storage.set('settingKey', 'newValue'); +``` + +##### Examples + +###### Example 1: Switch Setting + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + hideLocked = storage.get('hideLocked'); + + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + async parseNovel(novelPath: string): Promise { + // Use the setting value + if (this.hideLocked) { + // Filter out locked chapters + } + ... + } + ... +} +``` + +###### Example 2: Text Settings + +```ts +class ExamplePlugin implements Plugin.PluginBase { + ... + site = storage.get('url'); + email = storage.get('email'); + password = storage.get('password'); + + pluginSettings = { + url: { + value: '', + label: 'URL', + // type: 'Text' is optional + }, + email: { + value: '', + label: 'Email', + type: 'Text', + }, + password: { + value: '', + label: 'Password', + // type defaults to 'Text' if omitted + }, + }; + + async makeRequest(url: string): Promise { + return await fetchApi(url, { + headers: { + 'Authorization': `Basic ${this.btoa(this.email + ':' + this.password)}`, + }, + Referer: this.site, + }).then(res => res.text()); + } + ... +} +``` + +--- + +### Using Cheerio + +### Custom fetching functions diff --git a/docs/komga-plugin.md b/docs/komga-plugin.md new file mode 100644 index 000000000..360bc8a00 --- /dev/null +++ b/docs/komga-plugin.md @@ -0,0 +1,5 @@ +1. Install Komga plugin; +2. In the installed plugins page press the cog icon to open the plugin settings; +3. Fill in the required information (email, password and your komga server url); +4. Press save and restart the app; +5. The komga plugin will now work like the other plugins. \ No newline at end of file diff --git a/docs/plugin-template.ts b/docs/plugin-template.ts new file mode 100644 index 000000000..fa34cff9b --- /dev/null +++ b/docs/plugin-template.ts @@ -0,0 +1,95 @@ +import { fetchApi, fetchProto, fetchText } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +// import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +// import { storage, localStorage, sessionStorage } from '@libs/storage'; +// import { encode, decode } from 'urlencode'; +// import dayjs from 'dayjs'; +// import { Parser } from 'htmlparser2'; + +class TemplatePlugin implements Plugin.PluginBase { + id = ''; + name = ''; + icon = ''; + site = 'https://example.com'; + version = '1.0.0'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const novels: Plugin.NovelItem[] = []; + + /** Add your fetching code here */ + novels.push({ + name: 'Novel1', + path: '/novels/1', + cover: defaultCover, + }); + return novels; + } + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + // TODO: get here data from the site and + // un-comment and fill-in the relevant fields + + // novel.name = ''; + // novel.artist = ''; + // novel.author = ''; + novel.cover = defaultCover; + // novel.genres = ''; + // novel.status = NovelStatus.Completed; + // novel.summary = ''; + + const chapters: Plugin.ChapterItem[] = []; + + // TODO: here parse the chapter list + + // TODO: add each chapter to the list using + const chapter: Plugin.ChapterItem = { + name: '', + path: '', + releaseTime: '', + chapterNumber: 0, + }; + chapters.push(chapter); + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + // parse chapter text here + const chapterText = ''; + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const novels: Plugin.NovelItem[] = []; + + // get novels using the search term + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/chapter/') + path; +} + +export default new TemplatePlugin(); diff --git a/docs/quickstart.md b/docs/quickstart.md new file mode 100644 index 000000000..22cf8a7d0 --- /dev/null +++ b/docs/quickstart.md @@ -0,0 +1,30 @@ +# Quick start + +1. [Requirements](#requirements) +2. [Single plugin guide](#quick-guide) +3. [Multi-src guide](#creating-multi-src-plugins) + +### Requirements + +- [git](https://git-scm.com/doc/ext) basics +- Typescript or Javascript basics +- Node >=22 +- Installing the dependencies with `npm i` + +### Guide + +1. Create plugin script in `/plugins` [(learn more)](#creating-plugin-script) +2. Copy code from [plugin-template.ts](./plugin-template.ts) +3. Start coding [(documentation)](./docs.md) + +#### Creating plugin script + +1. Remember to create your plugin inside the language folder corresponding to the language of the novels +2. File should have the `.ts` extension + Example `plugins/english/nobleMTL.ts` +3. Add an icon to `public/static/src///icon.png` + +> [!WARNING] +> Icon size should be 96x96px! + +### Creating multi-source plugins diff --git a/docs/website-tutorial.md b/docs/website-tutorial.md new file mode 100644 index 000000000..e0194ad83 --- /dev/null +++ b/docs/website-tutorial.md @@ -0,0 +1,38 @@ +# Testing Website Tutorial + +A comprehensive guide to testing your LNReader plugins using the web interface. + +## Getting Started + +1. **Start the development server:** + + ```bash + npm run dev:start + ``` + +2. **Open your browser:** + Navigate to [localhost:3000](http://localhost:3000) + +3. **Select a plugin:** + Use the dropdown in the top navigation bar to select the plugin you want to test. + +## Features Overview + +The testing website provides five main sections to test different plugin functions: + +- **Headers** - Configure custom HTTP headers +- **Popular Novels** - Test `popularNovels()` with pagination and filters +- **Search Novels** - Test `searchNovels()` with search queries +- **Parse Novel** - Test `parseNovel()` with a novel path +- **Parse Chapter** - Test `parseChapter()` with a chapter path + +## Pre-Submission Testing + +Before submitting your plugin, verify that all five sections work without errors, multiple pages load, search returns accurate results, novel parsing extracts all metadata, chapter content is clean, filters work (if implemented), no console errors appear, paths are properly formatted, and images load correctly. + +## Need Help? + +- **Plugin Development:** See [docs.md](./docs.md) for API reference +- **Quick Start:** See [quickstart.md](./quickstart.md) for plugin creation +- **Issues:** Create a [GitHub issue](https://github.com/LNReader/lnreader-plugins/issues/new) +- **Community:** Join us on [Discord](https://discord.gg/QdcWN4MD63) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..30c4da5df --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,205 @@ +// @ts-check + +import eslint from '@eslint/js'; +import prettierConfig from 'eslint-config-prettier'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; + +const globalsHermes = [ + //hermes@0.72 + 'AggregateError', + 'Array', + 'ArrayBuffer', + 'BigInt', + 'BigInt64Array', + 'BigUint64Array', + 'Boolean', + 'DataView', + 'Date', + 'DebuggerInternal', + 'Error', + 'EvalError', + 'Float32Array', + 'Float64Array', + 'Function', + 'HermesInternal', + 'Infinity', + 'Int16Array', + 'Int32Array', + 'Int8Array', + 'JSON', + 'Map', + 'Math', + 'NaN', + 'Number', + 'Object', + 'Promise', + 'Proxy', + 'QuitError', + 'RangeError', + 'ReferenceError', + 'Reflect', + 'RegExp', + 'Set', + 'String', + 'Symbol', + 'SyntaxError', + 'TimeoutError', + 'TypeError', + 'URIError', + 'Uint16Array', + 'Uint32Array', + 'Uint8Array', + 'Uint8ClampedArray', + 'WeakMap', + 'WeakSet', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__', + '__proto__', + 'clearTimeout', + 'constructor', + 'createHeapSnapshot', + 'decodeURI', + 'decodeURIComponent', + 'encodeURI', + 'encodeURIComponent', + 'escape', + 'gc', + 'globalThis', + 'hasOwnProperty', + 'isFinite', + 'isNaN', + 'isPrototypeOf', + 'loadSegment', + 'parseFloat', + 'parseInt', + 'print', + 'propertyIsEnumerable', + 'setImmediate', + 'setTimeout', + 'toLocaleString', + 'toString', + 'undefined', + 'unescape', + 'valueOf', + //react-native + 'AbortController', + 'Blob', + 'ErrorUtils', + 'Event', + 'EventTarget', + 'File', + 'FileReader', + 'FormData', + 'Headers', + 'Intl', + 'URL', + 'URLSearchParams', + 'WebSocket', + 'XMLHttpRequest', + '__DEV__', + '__dirname', + '__fbBatchedBridgeConfig', + 'alert', + 'cancelAnimationFrame', + 'cancelIdleCallback', + 'clearImmediate', + 'clearInterval', + 'console', + 'document', + 'exports', + 'fetch', + 'global', + 'module', + 'navigator', + 'process', + 'queueMicrotask', + 'requestAnimationFrame', + 'requestIdleCallback', + 'require', + 'setInterval', + 'window', +].map(key => ({ [key]: 'readonly' })); + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + prettierConfig, + { + ignores: [ + '.js', + 'docs', + 'proxy_server.js', + 'plugins/*/*\\[*\\]*.ts', // Files with square brackets in their names + ], + }, + { + files: ['./plugins/*/*.ts', './plugins/multisrc/*/template.ts'], + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + 'no-case-declarations': 'warn', + 'no-undef': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@/lib/fetch*'], + message: 'Use @libs/fetch instead of @/lib/fetch', + }, + ], + }, + ], + }, + languageOptions: { + ecmaVersion: 5, + sourceType: 'module', + globals: Object.assign({}, ...globalsHermes), + }, + }, + { + files: ['**/*.{ts,tsx,mts,cts,js,cjs}'], + ignores: ['./plugins/*/*.ts', './plugins/multisrc/*/template.ts'], + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-unused-vars': 'warn', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/consistent-type-definitions': ['error', 'type'], + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-undef': 'error', + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['@/lib/fetch*'], + message: 'Use @libs/fetch instead of @/lib/fetch', + }, + ], + }, + ], + }, + languageOptions: { + globals: { + ...globals.serviceworker, + ...globals.browser, + ...globals.node, + }, + }, + }, + { + files: ['**/fictioneer/custom/*/*.js'], + rules: { + 'no-undef': 'off', + }, + }, +); diff --git a/index.html b/index.html new file mode 100644 index 000000000..b51f91623 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + Plugin Playground + + + +
+ + + diff --git a/package-lock.json b/package-lock.json index 91bba2f65..3cb3ca8fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,1582 +1,15276 @@ { - "name": "lnreader-backend", - "version": "1.0.0", - "lockfileVersion": 1, + "name": "lnreader-plugins", + "version": "3.0.0", + "lockfileVersion": 2, "requires": true, + "packages": { + "": { + "name": "lnreader-plugins", + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@fontsource/geist-mono": "^5.2.7", + "@fontsource/geist-sans": "^5.2.5", + "@noble/ciphers": "^2.1.1", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.1.14", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jszip": "^3.10.1", + "lucide-react": "^0.546.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14", + "terser": "^5.36.0", + "typescript": "^5.7.2" + }, + "devDependencies": { + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", + "@eslint/js": "^9.5.0", + "@fontsource/roboto": "^5.1.0", + "@mui/icons-material": "^6.1.9", + "@mui/material": "^6.1.9", + "@reduxjs/toolkit": "^2.4.0", + "@types/eslint__js": "^8.42.3", + "@types/eslint-config-prettier": "^6.11.3", + "@types/http-proxy": "^1.17.16", + "@types/node": "^24.8.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-window": "^1.8.8", + "@vitejs/plugin-react-swc": "^3.9.0", + "cheerio": "^1.0.0", + "dayjs": "^1.11.13", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.6.0", + "htmlparser2": "^9.1.0", + "http-proxy": "^1.18.1", + "husky": "^9.0.11", + "image-size": "^1.1.1", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-redux": "^9.1.2", + "react-virtuoso": "^4.12.2", + "react-window": "^1.8.10", + "sanitize-html": "^2.13.1", + "serve": "^14.2.4", + "tw-animate-css": "^1.4.0", + "typescript-eslint": "^7.14.1", + "urlencode": "^2.0.0", + "vite": "^6.3.2", + "vite-plugin-node-polyfills": "^0.26.0", + "zustand": "^5.0.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "dev": true, + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "dev": true, + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true + }, + "node_modules/@emotion/react": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", + "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true + }, + "node_modules/@emotion/styled": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz", + "integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "dev": true, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fontsource/geist-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/geist-mono/-/geist-mono-5.2.7.tgz", + "integrity": "sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/geist-sans": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/geist-sans/-/geist-sans-5.2.5.tgz", + "integrity": "sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "node_modules/@fontsource/roboto": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz", + "integrity": "sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==", + "dev": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mui/core-downloads-tracker": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.9.tgz", + "integrity": "sha512-TWqj7b1w5cmSz4H/uf+y2AHxAH4ldPR7D2bz0XVyn60GCAo/zRbRPx7cF8gTs/i7CiYeHzV6dtat0VpMwOtolw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.9.tgz", + "integrity": "sha512-AzlhIT51rdjkZ/EcUV2dbhNkNSUHIqCnNoUxodpiTw8buyAUBd+qnxg5OBSuPpun/ZEdSSB8Q7Uyh6zqjiMsEQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.1.9", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.9.tgz", + "integrity": "sha512-NwqIN0bdsgzSbZd5JFcC+2ez0XW/XNs8uiV2PDHrqQ4qf/FEasFJG1z6g8JbCN0YlTrHZekVb17X0Fv0qcYJfQ==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.1.9", + "@mui/system": "^6.1.9", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.9", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.1.9", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/private-theming": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.9.tgz", + "integrity": "sha512-7aum/O1RquBYhfwL/7egDyl9GqJgPM6hoJDFFBbhF6Sgv9yI9v4w3ArKUkuVvR0CtVj4NXRVMKEioh1bjUzvuA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.1.9", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.9.tgz", + "integrity": "sha512-xynSLlJRxHLzSfQaiDjkaTx8LiFb9ByVa7aOdwFnTxGWFMY1F+mkXwAUY4jDDE+MAxkWxlzzQE0wOohnsxhdQg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.9.tgz", + "integrity": "sha512-8x+RucnNp21gfFYsklCaZf0COXbv3+v0lrVuXONxvPEkESi2rwLlOi8UPJfcz6LxZOAX3v3oQ7qw18vnpgueRg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.1.9", + "@mui/styled-engine": "^6.1.9", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.9", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "dev": true, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.9.tgz", + "integrity": "sha512-N7uzBp7p2or+xanXn3aH2OTINC6F/Ru/U8h6amhRZEev8bJhKN86rIDIoxZZ902tj+09LXtH83iLxFMjMHyqNA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.19", + "@types/prop-types": "^15.7.13", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", + "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "dev": true, + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/core": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.22.tgz", + "integrity": "sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.11.22", + "@swc/core-darwin-x64": "1.11.22", + "@swc/core-linux-arm-gnueabihf": "1.11.22", + "@swc/core-linux-arm64-gnu": "1.11.22", + "@swc/core-linux-arm64-musl": "1.11.22", + "@swc/core-linux-x64-gnu": "1.11.22", + "@swc/core-linux-x64-musl": "1.11.22", + "@swc/core-win32-arm64-msvc": "1.11.22", + "@swc/core-win32-ia32-msvc": "1.11.22", + "@swc/core-win32-x64-msvc": "1.11.22" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.22.tgz", + "integrity": "sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.22.tgz", + "integrity": "sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.22.tgz", + "integrity": "sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.22.tgz", + "integrity": "sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.22.tgz", + "integrity": "sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.22.tgz", + "integrity": "sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.22.tgz", + "integrity": "sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.22.tgz", + "integrity": "sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.22.tgz", + "integrity": "sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.22.tgz", + "integrity": "sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz", + "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/eslint-config-prettier": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz", + "integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "18.2.66", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", + "integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==", + "devOptional": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "devOptional": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", + "devOptional": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.14.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@vitejs/plugin-react-swc": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.11.21" + }, + "peerDependencies": { + "vite": "^4 || ^5 || ^6" + } + }, + "node_modules/@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true, + "license": "MIT" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk-template/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=18.17" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clipboardy/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/clipboardy/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "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" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz", + "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dev": true, + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true, + "bin": { + "husky": "bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", + "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz", + "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "~1.33.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", + "dev": true + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true, + "license": "(WTFPL OR MIT)" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true + }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dev": true, + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtuoso": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.2.tgz", + "integrity": "sha512-9KiEc3uxD07qNrwb09PhPJKWfeNQ/Fw/TNKdZS7D3v4cDa6M/jg5lKLAUlRL7RluO8870cgLGM1T5pPKYEnprg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/registry-url": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ripemd160/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/ripemd160/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sanitize-html": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz", + "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==", + "dev": true, + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "bin": { + "serve": "build/main.js" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + } + }, + "node_modules/serve-handler/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serve/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/serve/node_modules/chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/serve/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "dev": true + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.14.1.tgz", + "integrity": "sha512-Eo1X+Y0JgGPspcANKjeR6nIqXl4VL5ldXLc15k4m9upq+eY5fhU2IueiEZL6jmHrKH8aCfbIvM/v3IrX5Hg99w==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "7.14.1", + "@typescript-eslint/parser": "7.14.1", + "@typescript-eslint/utils": "7.14.1" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/urlencode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/urlencode/-/urlencode-2.0.0.tgz", + "integrity": "sha512-K4+koEq4II9FqKKdLyMwfVFiWvTLJsdsIihXCprumjlOwpviO44E4hAhLYBLb6CEVTZh9hXXMTQHIT+Hwv5BPw==", + "dev": true, + "dependencies": { + "iconv-lite": "~0.6.3" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "dev": true, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/widest-line/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/widest-line/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "devOptional": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "dev": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + }, "dependencies": { - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", + "@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "requires": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "requires": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true + }, + "@babel/parser": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", + "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "dev": true, + "requires": { + "@babel/types": "^7.26.0" + } + }, + "@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/traverse": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", + "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/template": "^7.25.9", + "@babel/types": "^7.25.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", + "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "optional": true, + "requires": { + "tslib": "^2.4.0" + } + }, + "@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "@emotion/cache": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.13.5.tgz", + "integrity": "sha512-Z3xbtJ+UcK76eWkagZ1onvn/wAVb1GOMuR15s30Fm2wrMgC7jzpnO2JZXr4eujTTqoQFUrZIw/rT0c6Zzjca1g==", + "dev": true, + "requires": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "dev": true + }, + "@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "dev": true, + "requires": { + "@emotion/memoize": "^0.9.0" + } + }, + "@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "dev": true + }, + "@emotion/react": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.13.5.tgz", + "integrity": "sha512-6zeCUxUH+EPF1s+YF/2hPVODeV/7V07YU5x+2tfuRL8MdW6rv5vb2+CBEGTGwBdux0OIERcOS+RzxeK80k2DsQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + } + }, + "@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "dev": true, + "requires": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "dev": true + }, + "@emotion/styled": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.13.5.tgz", + "integrity": "sha512-gnOQ+nGLPvDXgIx119JqGalys64lhMdnNQA9TMxhDA4K0Hq5+++OE20Zs5GxiCV9r814xQ2K5WmtofSpHVW6BQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.1.0", + "@emotion/utils": "^1.4.2" + } + }, + "@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "dev": true + }, + "@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.1.0.tgz", + "integrity": "sha512-+wBOcIV5snwGgI2ya3u99D7/FJquOIniQT1IKyDsBmEgwvpxMNeS65Oib7OnE2d2aY+3BU4OiH+0Wchf8yk3Hw==", + "dev": true, + "requires": {} + }, + "@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "dev": true + }, + "@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "dev": true + }, + "@esbuild/aix-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.3.tgz", + "integrity": "sha512-W8bFfPA8DowP8l//sxjJLSLkD8iEjMc7cBVyP+u4cEv9sM7mdUCkgsj+t0n/BWPFtv7WWCN5Yzj0N6FJNUUqBQ==", + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.3.tgz", + "integrity": "sha512-PuwVXbnP87Tcff5I9ngV0lmiSu40xw1At6i3GsU77U7cjDDB4s0X2cyFuBiDa1SBk9DnvWwnGvVaGBqoFWPb7A==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.3.tgz", + "integrity": "sha512-XelR6MzjlZuBM4f5z2IQHK6LkK34Cvv6Rj2EntER3lwCBFdg6h2lKbtRjpTTsdEjD/WSe1q8UyPBXP1x3i/wYQ==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.3.tgz", + "integrity": "sha512-ogtTpYHT/g1GWS/zKM0cc/tIebFjm1F9Aw1boQ2Y0eUQ+J89d0jFY//s9ei9jVIlkYi8AfOjiixcLJSGNSOAdQ==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.3.tgz", + "integrity": "sha512-eESK5yfPNTqpAmDfFWNsOhmIOaQA59tAcF/EfYvo5/QWQCzXn5iUSOnqt3ra3UdzBv073ykTtmeLJZGt3HhA+w==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.3.tgz", + "integrity": "sha512-Kd8glo7sIZtwOLcPbW0yLpKmBNWMANZhrC1r6K++uDR2zyzb6AeOYtI6udbtabmQpFaxJ8uduXMAo1gs5ozz8A==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.3.tgz", + "integrity": "sha512-EJiyS70BYybOBpJth3M0KLOus0n+RRMKTYzhYhFeMwp7e/RaajXvP+BWlmEXNk6uk+KAu46j/kaQzr6au+JcIw==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.3.tgz", + "integrity": "sha512-Q+wSjaLpGxYf7zC0kL0nDlhsfuFkoN+EXrx2KSB33RhinWzejOd6AvgmP5JbkgXKmjhmpfgKZq24pneodYqE8Q==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.3.tgz", + "integrity": "sha512-dUOVmAUzuHy2ZOKIHIKHCm58HKzFqd+puLaS424h6I85GlSDRZIA5ycBixb3mFgM0Jdh+ZOSB6KptX30DD8YOQ==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.3.tgz", + "integrity": "sha512-xCUgnNYhRD5bb1C1nqrDV1PfkwgbswTTBRbAd8aH5PhYzikdf/ddtsYyMXFfGSsb/6t6QaPSzxtbfAZr9uox4A==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.3.tgz", + "integrity": "sha512-yplPOpczHOO4jTYKmuYuANI3WhvIPSVANGcNUeMlxH4twz/TeXuzEP41tGKNGWJjuMhotpGabeFYGAOU2ummBw==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.3.tgz", + "integrity": "sha512-P4BLP5/fjyihmXCELRGrLd793q/lBtKMQl8ARGpDxgzgIKJDRJ/u4r1A/HgpBpKpKZelGct2PGI4T+axcedf6g==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.3.tgz", + "integrity": "sha512-eRAOV2ODpu6P5divMEMa26RRqb2yUoYsuQQOuFUexUoQndm4MdpXXDBbUoKIc0iPa4aCO7gIhtnYomkn2x+bag==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.3.tgz", + "integrity": "sha512-ZC4jV2p7VbzTlnl8nZKLcBkfzIf4Yad1SJM4ZMKYnJqZFD4rTI+pBG65u8ev4jk3/MPwY9DvGn50wi3uhdaghg==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.3.tgz", + "integrity": "sha512-LDDODcFzNtECTrUUbVCs6j9/bDVqy7DDRsuIXJg6so+mFksgwG7ZVnTruYi5V+z3eE5y+BJZw7VvUadkbfg7QA==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.3.tgz", + "integrity": "sha512-s+w/NOY2k0yC2p9SLen+ymflgcpRkvwwa02fqmAwhBRI3SC12uiS10edHHXlVWwfAagYSY5UpmT/zISXPMW3tQ==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.3.tgz", + "integrity": "sha512-nQHDz4pXjSDC6UfOE1Fw9Q8d6GCAd9KdvMZpfVGWSJztYCarRgSDfOVBY5xwhQXseiyxapkiSJi/5/ja8mRFFA==", + "optional": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.3.tgz", + "integrity": "sha512-1QaLtOWq0mzK6tzzp0jRN3eccmN3hezey7mhLnzC6oNlJoUJz4nym5ZD7mDnS/LZQgkrhEbEiTn515lPeLpgWA==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.3.tgz", + "integrity": "sha512-i5Hm68HXHdgv8wkrt+10Bc50zM0/eonPb/a/OFVfB6Qvpiirco5gBA5bz7S2SHuU+Y4LWn/zehzNX14Sp4r27g==", + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.3.tgz", + "integrity": "sha512-zGAVApJEYTbOC6H/3QBr2mq3upG/LBEXr85/pTtKiv2IXcgKV0RT0QA/hSXZqSvLEpXeIxah7LczB4lkiYhTAQ==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.3.tgz", + "integrity": "sha512-fpqctI45NnCIDKBH5AXQBsD0NDPbEFczK98hk/aa6HJxbl+UtLkJV2+Bvy5hLSLk3LHmqt0NTkKNso1A9y1a4w==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.3.tgz", + "integrity": "sha512-ROJhm7d8bk9dMCUZjkS8fgzsPAZEjtRJqCAmVgB0gMrvG7hfmPmz9k1rwO4jSiblFjYmNvbECL9uhaPzONMfgA==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.3.tgz", + "integrity": "sha512-YWcow8peiHpNBiIXHwaswPnAXLsLVygFwCB3A7Bh5jRkIBFWHGmNQ48AlX4xDvQNoMZlPYzjVOQDYEzWCqufMQ==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.3.tgz", + "integrity": "sha512-qspTZOIGoXVS4DpNqUYUs9UxVb04khS1Degaw/MnfMe7goQ3lTfQ13Vw4qY/Nj0979BGvMRpAYbs/BAxEvU8ew==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.3.tgz", + "integrity": "sha512-ICgUR+kPimx0vvRzf+N/7L7tVSQeE3BYY+NhHRHXS1kBuPO7z2+7ea2HbhDyZdTephgvNvKrlDDKUexuCVBVvg==", + "optional": true + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.1.tgz", + "integrity": "sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "@eslint/js": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.5.0.tgz", + "integrity": "sha512-A7+AOT2ICkodvtsWnxZP4Xxk3NbZ3VMHd8oihydLRGrJgqqdEz1qSeEgXYyT/Cu8h1TWWsQRejIx48mtjZ5y1w==", + "dev": true + }, + "@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "requires": { + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "requires": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "requires": { + "@floating-ui/dom": "^1.7.4" + } + }, + "@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==" + }, + "@fontsource/geist-mono": { + "version": "5.2.7", + "resolved": "https://registry.npmjs.org/@fontsource/geist-mono/-/geist-mono-5.2.7.tgz", + "integrity": "sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==" + }, + "@fontsource/geist-sans": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/@fontsource/geist-sans/-/geist-sans-5.2.5.tgz", + "integrity": "sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==" + }, + "@fontsource/roboto": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.0.tgz", + "integrity": "sha512-cFRRC1s6RqPygeZ8Uw/acwVHqih8Czjt6Q0MwoUoDe9U3m4dH1HmNDRBZyqlMSFwgNAUKgFImncKdmDHyKpwdg==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "dev": true + }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "requires": { + "minipass": "^7.0.4" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@mui/core-downloads-tracker": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.9.tgz", + "integrity": "sha512-TWqj7b1w5cmSz4H/uf+y2AHxAH4ldPR7D2bz0XVyn60GCAo/zRbRPx7cF8gTs/i7CiYeHzV6dtat0VpMwOtolw==", + "dev": true + }, + "@mui/icons-material": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.9.tgz", + "integrity": "sha512-AzlhIT51rdjkZ/EcUV2dbhNkNSUHIqCnNoUxodpiTw8buyAUBd+qnxg5OBSuPpun/ZEdSSB8Q7Uyh6zqjiMsEQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0" + } + }, + "@mui/material": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.9.tgz", + "integrity": "sha512-NwqIN0bdsgzSbZd5JFcC+2ez0XW/XNs8uiV2PDHrqQ4qf/FEasFJG1z6g8JbCN0YlTrHZekVb17X0Fv0qcYJfQ==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.1.9", + "@mui/system": "^6.1.9", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.9", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.11", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^18.3.1", + "react-transition-group": "^4.4.5" + } + }, + "@mui/private-theming": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.9.tgz", + "integrity": "sha512-7aum/O1RquBYhfwL/7egDyl9GqJgPM6hoJDFFBbhF6Sgv9yI9v4w3ArKUkuVvR0CtVj4NXRVMKEioh1bjUzvuA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.1.9", + "prop-types": "^15.8.1" + } + }, + "@mui/styled-engine": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.9.tgz", + "integrity": "sha512-xynSLlJRxHLzSfQaiDjkaTx8LiFb9ByVa7aOdwFnTxGWFMY1F+mkXwAUY4jDDE+MAxkWxlzzQE0wOohnsxhdQg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/system": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.9.tgz", + "integrity": "sha512-8x+RucnNp21gfFYsklCaZf0COXbv3+v0lrVuXONxvPEkESi2rwLlOi8UPJfcz6LxZOAX3v3oQ7qw18vnpgueRg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.1.9", + "@mui/styled-engine": "^6.1.9", + "@mui/types": "^7.2.19", + "@mui/utils": "^6.1.9", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + } + }, + "@mui/types": { + "version": "7.2.19", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.19.tgz", + "integrity": "sha512-6XpZEM/Q3epK9RN8ENoXuygnqUQxE+siN/6rGRi2iwJPgBUR25mphYQ9ZI87plGh58YoZ5pp40bFvKYOCDJ3tA==", + "dev": true, + "requires": {} + }, + "@mui/utils": { + "version": "6.1.9", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.9.tgz", + "integrity": "sha512-N7uzBp7p2or+xanXn3aH2OTINC6F/Ru/U8h6amhRZEev8bJhKN86rIDIoxZZ902tj+09LXtH83iLxFMjMHyqNA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.19", + "@types/prop-types": "^15.7.13", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^18.3.1" + } + }, + "@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==" + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true + }, + "@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true + }, + "@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true + }, + "@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true + }, + "@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true + }, + "@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "requires": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true + }, + "@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true + }, + "@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true + }, + "@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true + }, + "@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true + }, + "@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==" + }, + "@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==" + }, + "@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + } + }, + "@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "requires": {} + }, + "@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "requires": {} + }, + "@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + } + }, + "@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "requires": {} + }, + "@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + } + }, + "@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "requires": {} + }, + "@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + } + }, + "@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "requires": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + } + }, + "@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "requires": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "requires": { + "@radix-ui/react-slot": "1.2.3" + } + }, + "@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "requires": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + } + }, + "@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "requires": { + "@radix-ui/react-compose-refs": "1.1.2" + } + }, + "@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + } + }, + "@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + } + }, + "@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "requires": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + } + }, + "@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "requires": {} + }, + "@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "requires": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "requires": { + "@radix-ui/react-use-callback-ref": "1.1.1" + } + }, + "@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "requires": {} + }, + "@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "requires": {} + }, + "@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "requires": { + "@radix-ui/rect": "1.1.1" + } + }, + "@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "requires": { + "@radix-ui/react-use-layout-effect": "1.1.1" + } + }, + "@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "requires": { + "@radix-ui/react-primitive": "2.1.3" + } + }, + "@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==" + }, + "@reduxjs/toolkit": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", + "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "dev": true, + "requires": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + } + }, + "@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "requires": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + } + }, + "@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "requires": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "dependencies": { + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true + } + } + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "optional": true + }, + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "optional": true + }, + "@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "optional": true + }, + "@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "optional": true + }, + "@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "optional": true + }, + "@swc/core": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.22.tgz", + "integrity": "sha512-mjPYbqq8XjwqSE0hEPT9CzaJDyxql97LgK4iyvYlwVSQhdN1uK0DBG4eP9PxYzCS2MUGAXB34WFLegdUj5HGpg==", + "dev": true, + "requires": { + "@swc/core-darwin-arm64": "1.11.22", + "@swc/core-darwin-x64": "1.11.22", + "@swc/core-linux-arm-gnueabihf": "1.11.22", + "@swc/core-linux-arm64-gnu": "1.11.22", + "@swc/core-linux-arm64-musl": "1.11.22", + "@swc/core-linux-x64-gnu": "1.11.22", + "@swc/core-linux-x64-musl": "1.11.22", + "@swc/core-win32-arm64-msvc": "1.11.22", + "@swc/core-win32-ia32-msvc": "1.11.22", + "@swc/core-win32-x64-msvc": "1.11.22", + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.21" + } + }, + "@swc/core-darwin-arm64": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.11.22.tgz", + "integrity": "sha512-upSiFQfo1TE2QM3+KpBcp5SrOdKKjoc+oUoD1mmBDU2Wv4Bjjv16Z2I5ADvIqMV+b87AhYW+4Qu6iVrQD7j96Q==", + "dev": true, + "optional": true + }, + "@swc/core-darwin-x64": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.11.22.tgz", + "integrity": "sha512-8PEuF/gxIMJVK21DjuCOtzdqstn2DqnxVhpAYfXEtm3WmMqLIOIZBypF/xafAozyaHws4aB/5xmz8/7rPsjavw==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm-gnueabihf": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.11.22.tgz", + "integrity": "sha512-NIPTXvqtn9e7oQHgdaxM9Z/anHoXC3Fg4ZAgw5rSGa1OlnKKupt5sdfJamNggSi+eAtyoFcyfkgqHnfe2u63HA==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-gnu": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.11.22.tgz", + "integrity": "sha512-xZ+bgS60c5r8kAeYsLNjJJhhQNkXdidQ277pUabSlu5GjR0CkQUPQ+L9hFeHf8DITEqpPBPRiAiiJsWq5eqMBg==", + "dev": true, + "optional": true + }, + "@swc/core-linux-arm64-musl": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.11.22.tgz", + "integrity": "sha512-JhrP/q5VqQl2eJR0xKYIkKTPjgf8CRsAmRnjJA2PtZhfQ543YbYvUqxyXSRyBOxdyX8JwzuAxIPEAlKlT7PPuQ==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-gnu": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.11.22.tgz", + "integrity": "sha512-htmAVL+U01gk9GyziVUP0UWYaUQBgrsiP7Ytf6uDffrySyn/FclUS3MDPocNydqYsOpj3OpNKPxkaHK+F+X5fg==", + "dev": true, + "optional": true + }, + "@swc/core-linux-x64-musl": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.11.22.tgz", + "integrity": "sha512-PL0VHbduWPX+ANoyOzr58jBiL2VnD0xGSFwPy7NRZ1Pr6SNWm4jw3x2u6RjLArGhS5EcWp64BSk9ZxqmTV3FEg==", + "dev": true, + "optional": true + }, + "@swc/core-win32-arm64-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.11.22.tgz", + "integrity": "sha512-moJvFhhTVGoMeEThtdF7hQog80Q00CS06v5uB+32VRuv+I31+4WPRyGlTWHO+oY4rReNcXut/mlDHPH7p0LdFg==", + "dev": true, + "optional": true + }, + "@swc/core-win32-ia32-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.11.22.tgz", + "integrity": "sha512-/jnsPJJz89F1aKHIb5ScHkwyzBciz2AjEq2m9tDvQdIdVufdJ4SpEDEN9FqsRNRLcBHjtbLs6bnboA+B+pRFXw==", + "dev": true, + "optional": true + }, + "@swc/core-win32-x64-msvc": { + "version": "1.11.22", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.11.22.tgz", + "integrity": "sha512-lc93Y8Mku7LCFGqIxJ91coXZp2HeoDcFZSHCL90Wttg5xhk5xVM9uUCP+OdQsSsEixLF34h5DbT9ObzP8rAdRw==", + "dev": true, + "optional": true + }, + "@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true + }, + "@swc/types": { + "version": "0.1.21", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.21.tgz", + "integrity": "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ==", + "dev": true, + "requires": { + "@swc/counter": "^0.1.3" + } + }, + "@tailwindcss/node": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.14.tgz", + "integrity": "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==", + "requires": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.0", + "lightningcss": "1.30.1", + "magic-string": "^0.30.19", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.14" + } + }, + "@tailwindcss/oxide": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.14.tgz", + "integrity": "sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==", + "requires": { + "@tailwindcss/oxide-android-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-arm64": "4.1.14", + "@tailwindcss/oxide-darwin-x64": "4.1.14", + "@tailwindcss/oxide-freebsd-x64": "4.1.14", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.14", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.14", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.14", + "@tailwindcss/oxide-linux-x64-musl": "4.1.14", + "@tailwindcss/oxide-wasm32-wasi": "4.1.14", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.14", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.14", + "detect-libc": "^2.0.4", + "tar": "^7.5.1" + } + }, + "@tailwindcss/oxide-android-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.14.tgz", + "integrity": "sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==", + "optional": true + }, + "@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.14.tgz", + "integrity": "sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==", + "optional": true + }, + "@tailwindcss/oxide-darwin-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.14.tgz", + "integrity": "sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==", + "optional": true + }, + "@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.14.tgz", + "integrity": "sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.14.tgz", + "integrity": "sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.14.tgz", + "integrity": "sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==", + "optional": true + }, + "@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.14.tgz", + "integrity": "sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==", + "optional": true + }, + "@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.14.tgz", + "integrity": "sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==", + "optional": true + }, + "@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz", + "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==", + "optional": true + }, + "@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.14.tgz", + "integrity": "sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==", + "optional": true, + "requires": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.0.5", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + } + }, + "@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.14.tgz", + "integrity": "sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==", + "optional": true + }, + "@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.14.tgz", + "integrity": "sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==", + "optional": true + }, + "@tailwindcss/vite": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.14.tgz", + "integrity": "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==", + "requires": { + "@tailwindcss/node": "4.1.14", + "@tailwindcss/oxide": "4.1.14", + "tailwindcss": "4.1.14" + } + }, + "@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "requires": { + "@types/eslint": "*" + } + }, + "@types/eslint-config-prettier": { + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/@types/eslint-config-prettier/-/eslint-config-prettier-6.11.3.tgz", + "integrity": "sha512-3wXCiM8croUnhg9LdtZUJQwNcQYGWxxdOWDjPe1ykCqJFPVpzAKfs/2dgSoCtAvdPeaponcWPI7mPcGGp9dkKQ==", + "dev": true + }, + "@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + }, + "@types/http-proxy": { + "version": "1.17.16", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", + "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "@types/node": { + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", + "devOptional": true, + "requires": { + "undici-types": "~7.14.0" + } + }, + "@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "devOptional": true + }, + "@types/react": { + "version": "18.2.66", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.66.tgz", + "integrity": "sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==", + "devOptional": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "@types/react-dom": { + "version": "18.2.22", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.22.tgz", + "integrity": "sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==", + "devOptional": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==", + "devOptional": true + }, + "@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/parser": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" + } + }, + "@typescript-eslint/type-utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + } + }, + "@typescript-eslint/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, + "@typescript-eslint/utils": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "7.14.1", + "eslint-visitor-keys": "^3.4.3" + } + }, + "@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "@vitejs/plugin-react-swc": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.9.0.tgz", + "integrity": "sha512-jYFUSXhwMCYsh/aQTgSGLIN3Foz5wMbH9ahb0Zva//UzwZYbMiZd7oT3AU9jHT9DLswYDswsRwPU9jVF3yA48Q==", + "dev": true, + "requires": { + "@swc/core": "^1.11.21" + } + }, + "@zeit/schemas": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@zeit/schemas/-/schemas-2.36.0.tgz", + "integrity": "sha512-7kjMwcChYEzMKjeex9ZFXkt1AyNov9R5HZtjBKVsmVpw7pa7ZtlCGvCBC2vnnXctaYN+aRI61HjIqeetZW5ROg==", + "dev": true + }, + "acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==" + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "dev": true, + "requires": { + "string-width": "^4.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "requires": { + "environment": "^1.0.0" + } + }, + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "requires": { + "tslib": "^2.0.0" + } + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } + } + }, + "assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "requires": { + "possible-typed-array-names": "^1.0.0" + } + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "dev": true, + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "dev": true + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "boxen": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.0.0.tgz", + "integrity": "sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==", + "dev": true, + "requires": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.0", + "chalk": "^5.0.1", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "brace-expansion": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true + }, + "browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "requires": { + "resolve": "^1.17.0" + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "requires": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + } + }, + "browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "dev": true, + "requires": { + "bn.js": "^5.2.2", + "browserify-rsa": "^4.1.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.6.1", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.9", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true + }, + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true + }, + "chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "requires": { + "chalk": "^4.1.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "cheerio": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", + "dev": true, + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^6.19.5", + "whatwg-mimetype": "^4.0.0" + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + } + }, + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==" + }, + "cipher-base": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz", + "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + } + }, + "class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "requires": { + "clsx": "^2.1.1" + } + }, + "cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "dev": true + }, + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "requires": { + "restore-cursor": "^5.0.0" + } + }, + "cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + } + }, + "clipboardy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-3.0.0.tgz", + "integrity": "sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==", + "dev": true, + "requires": { + "arch": "^2.2.0", + "execa": "^5.1.1", + "is-wsl": "^2.2.0" + }, + "dependencies": { + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + } + } + }, + "clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true + }, + "content-disposition": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", + "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", + "dev": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "dependencies": { + "yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true + } + } + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "requires": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + } + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "devOptional": true + }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "dev": true + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true + }, + "define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "requires": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + } + }, + "define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "requires": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" + }, + "detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } + } + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "encoding-sniffer": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", + "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", + "dev": true, + "requires": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + } + }, + "enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "esbuild": { + "version": "0.25.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.3.tgz", + "integrity": "sha512-qKA6Pvai73+M2FtftpNKRxJ78GIjmFXFxd/1DVBqGo/qNhLSfv+G12n9pNoWdytJC8U00TrViOwpjT0zgqQS8Q==", + "requires": { + "@esbuild/aix-ppc64": "0.25.3", + "@esbuild/android-arm": "0.25.3", + "@esbuild/android-arm64": "0.25.3", + "@esbuild/android-x64": "0.25.3", + "@esbuild/darwin-arm64": "0.25.3", + "@esbuild/darwin-x64": "0.25.3", + "@esbuild/freebsd-arm64": "0.25.3", + "@esbuild/freebsd-x64": "0.25.3", + "@esbuild/linux-arm": "0.25.3", + "@esbuild/linux-arm64": "0.25.3", + "@esbuild/linux-ia32": "0.25.3", + "@esbuild/linux-loong64": "0.25.3", + "@esbuild/linux-mips64el": "0.25.3", + "@esbuild/linux-ppc64": "0.25.3", + "@esbuild/linux-riscv64": "0.25.3", + "@esbuild/linux-s390x": "0.25.3", + "@esbuild/linux-x64": "0.25.3", + "@esbuild/netbsd-arm64": "0.25.3", + "@esbuild/netbsd-x64": "0.25.3", + "@esbuild/openbsd-arm64": "0.25.3", + "@esbuild/openbsd-x64": "0.25.3", + "@esbuild/sunos-x64": "0.25.3", + "@esbuild/win32-arm64": "0.25.3", + "@esbuild/win32-ia32": "0.25.3", + "@esbuild/win32-x64": "0.25.3" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "dependencies": { + "@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true + }, + "fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "requires": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true + }, + "follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true + }, + "for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "requires": { + "is-callable": "^1.2.7" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true + }, + "get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.6.0.tgz", + "integrity": "sha512-UzcJi88Hw//CurUIRa9Jxb0vgOCcuD/MNjwmXp633cyaRKkCWACkoqHCtfZv43b1kqXGg/fpOa8bwgacCeXsVg==", "dev": true }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { - "defer-to-connect": "^1.0.1" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "es-define-property": "^1.0.0" } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "has-symbols": "^1.0.3" } }, - "ansi-align": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", - "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", "dev": true, "requires": { - "string-width": "^3.0.0" + "react-is": "^16.7.0" }, "dependencies": { - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true } } }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + } + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", "dev": true }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true + }, + "husky": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", + "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "dev": true + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true + }, + "image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", "dev": true, "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "queue": "6.0.2" } }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, "requires": { - "safer-buffer": "~2.1.0" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "requires": { - "tweetnacl": "^0.14.3" + "hasown": "^2.0.0" } }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" } }, - "boolbase": { + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + }, + "is-port-reachable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-port-reachable/-/is-port-reachable-4.0.0.tgz", + "integrity": "sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==", + "dev": true + }, + "is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, + "is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true + }, + "is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "requires": { + "which-typed-array": "^1.1.16" + } + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true + }, + "jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "requires": { + "detect-libc": "^2.0.3", + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "optional": true + }, + "lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "optional": true + }, + "lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "optional": true + }, + "lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "optional": true + }, + "lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "optional": true + }, + "lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "optional": true + }, + "lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "optional": true + }, + "lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "optional": true + }, + "lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "optional": true + }, + "lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "optional": true + }, + "lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "lint-staged": { + "version": "15.5.1", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.1.tgz", + "integrity": "sha512-6m7u8mue4Xn6wK6gZvSCQwBvMBR36xfY24nF5bMTf2MHDYG6S3yhJuOgdYVw99hsjyDt2d4z168b3naI8+NWtQ==", "dev": true, "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" + "chalk": "^5.4.1", + "commander": "^13.1.0", + "debug": "^4.4.0", + "execa": "^8.0.1", + "lilconfig": "^3.1.3", + "listr2": "^8.2.5", + "micromatch": "^4.0.8", + "pidtree": "^0.6.0", + "string-argv": "^0.3.2", + "yaml": "^2.7.0" } }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "listr2": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.2.tgz", + "integrity": "sha512-vsBzcU4oE+v0lj4FhVLzr9dBTv4/fHIa57l+GCwovP8MoFNZJTOhGU8PXd4v2VJCbECAaijBiHntiekFMLvo0g==", "dev": true, "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" } }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "p-locate": "^5.0.0" } }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true }, - "cacheable-request": { + "log-update": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, "requires": { - "pump": "^3.0.0" + "get-east-asian-width": "^1.0.0" } }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - } - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" } } } }, - "cheerio": { - "version": "1.0.0-rc.5", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.5.tgz", - "integrity": "sha512-yoqps/VCaZgN4pfXtenwHROTp8NG6/Hlt4Jpz2FEP0ZJQ+ZUkVDd0hAPDNKhj3nakpfPt/CNs57yEtxD1bXQiw==", + "long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "dev": true + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "requires": { - "cheerio-select-tmp": "^0.1.0", - "dom-serializer": "~1.2.0", - "domhandler": "^4.0.0", - "entities": "~2.1.0", - "htmlparser2": "^6.0.0", - "parse5": "^6.0.0", - "parse5-htmlparser2-tree-adapter": "^6.0.0" + "js-tokens": "^3.0.0 || ^4.0.0" } }, - "cheerio-select-tmp": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select-tmp/-/cheerio-select-tmp-0.1.0.tgz", - "integrity": "sha512-kx/pq9hxLo6FhjiYqUheSOV0Eb729ZwkXXPrPTeK6kl/VMgaUlsYoAOv3nOJZcHk++V9pI17YNNngtbLVPTB9A==", + "lucide-react": { + "version": "0.546.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.546.0.tgz", + "integrity": "sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==", + "requires": {} + }, + "magic-string": { + "version": "0.30.19", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", + "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==", "requires": { - "css-select": "^3.1.2", - "css-what": "^4.0.0", - "domelementtype": "^2.1.0", - "domhandler": "^4.0.0", - "domutils": "^2.4.4" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "chokidar": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz", - "integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==", + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.1.2", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, - "ci-info": { + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "dev": true + }, + "merge-stream": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true }, - "cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "requires": { - "mimic-response": "^1.0.0" + "braces": "^3.0.3", + "picomatch": "^2.3.1" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dev": true, "requires": { - "color-name": "~1.1.4" + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "dev": true }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, "requires": { - "delayed-stream": "~1.0.0" + "mime-db": "~1.33.0" + }, + "dependencies": { + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + } } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true }, - "configstore": { + "mimic-function": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" + "brace-expansion": "^1.1.7" } }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==" + }, + "minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", "requires": { - "safe-buffer": "5.1.2" + "minipass": "^7.1.2" } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" - }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + "nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true }, - "css-select": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-3.1.2.tgz", - "integrity": "sha512-qmss1EihSuBNWNNhHjxzxSfJoFBM/lERB/Q4EnsJQQC62R2evJDW481091oAdOr9uh46/0n4nrg0It5cAnj1RA==", + "node-stdlib-browser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz", + "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==", + "dev": true, "requires": { - "boolbase": "^1.0.0", - "css-what": "^4.0.0", - "domhandler": "^4.0.0", - "domutils": "^2.4.3", - "nth-check": "^2.0.0" + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.12.1", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + } } }, - "css-what": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-4.0.0.tgz", - "integrity": "sha512-teijzG7kwYfNVsUh2H/YN62xW3KK9YhXEgSlbxMlcyjPNvdKJqFx5lrwlJgoFP1ZHlB89iGDlo/JyshKeRhv5A==" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, "requires": { - "assert-plus": "^1.0.0" + "path-key": "^4.0.0" + }, + "dependencies": { + "path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true + } } }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, "requires": { - "ms": "2.0.0" + "boolbase": "^1.0.0" } }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true + }, + "object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true + }, + "object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, "requires": { - "mimic-response": "^1.0.0" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" } }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true + "object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + "onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "requires": { + "mimic-fn": "^4.0.0" + } }, - "dom-serializer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", - "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", + "optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "entities": "^2.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" } }, - "domelementtype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", - "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==" + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true }, - "domhandler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", - "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { - "domelementtype": "^2.1.0" + "yocto-queue": "^0.1.0" } }, - "domutils": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz", - "integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==", + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0" + "p-limit": "^3.0.2" } }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "requires": { - "is-obj": "^2.0.0" + "callsites": "^3.0.0" } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" } }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", "dev": true }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + "parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "requires": { + "entities": "^4.5.0" + } }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "requires": { - "once": "^1.4.0" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" } }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + "parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "requires": { + "parse5": "^7.0.0" + } }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" - }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - } + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", + "dev": true }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "path-to-regexp": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", + "dev": true }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pbkdf2": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz", + "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==", "dev": true, "requires": { - "to-regex-range": "^5.0.1" + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "ripemd160": "^2.0.3", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.12", + "to-buffer": "^1.2.1" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true + }, + "pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true + }, + "pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" + "find-up": "^5.0.0" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, "requires": { - "assert-plus": "^1.0.0" + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + }, + "dependencies": { + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + } } }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", + "protobufjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz", + "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" } }, - "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, "requires": { - "ini": "1.3.7" + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "dev": true + } } }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "dev": true, "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" + "side-channel": "^1.1.0" } }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", "dev": true }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" + "inherits": "~2.0.3" } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "has-yarn": { + "randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } }, - "htmlparser2": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz", - "integrity": "sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw==", + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.4.4", - "entities": "^2.0.0" + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" } }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "range-parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", + "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", "dev": true }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true + } } }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "loose-envify": "^1.1.0" } }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "requires": { - "safer-buffer": ">= 2.1.2 < 3" + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" } }, - "ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", - "dev": true - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", - "dev": true - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "dev": true, + "requires": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + } }, - "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true + "react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "requires": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + } }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + "react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "requires": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + } }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "requires": { - "binary-extensions": "^2.0.0" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" } }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", "dev": true, "requires": { - "ci-info": "^2.0.0" + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" } }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true + "react-virtuoso": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.2.tgz", + "integrity": "sha512-9KiEc3uxD07qNrwb09PhPJKWfeNQ/Fw/TNKdZS7D3v4cDa6M/jg5lKLAUlRL7RluO8870cgLGM1T5pPKYEnprg==", + "dev": true, + "requires": {} }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", "dev": true, "requires": { - "is-extglob": "^2.1.1" + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" } }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", + "redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "dev": true }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "requires": {} }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", + "dev": true, "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "rc": "^1.1.6", + "safe-buffer": "^5.0.1" } }, - "keyv": { + "registry-url": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", "dev": true, "requires": { - "json-buffer": "3.0.0" + "rc": "^1.0.1" } }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "dev": true + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "requires": { - "package-json": "^6.3.0" + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" } }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "requires": { - "semver": "^6.0.0" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "requires": { + "mimic-function": "^5.0.0" + } } } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "requires": { - "mime-db": "1.44.0" - } + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", "dev": true }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { - "brace-expansion": "^1.1.7" + "glob": "^7.1.3" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "nodemon": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz", - "integrity": "sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ==", + "ripemd160": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz", + "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==", "dev": true, "requires": { - "chokidar": "^3.2.2", - "debug": "^3.2.6", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.0.4", - "pstree.remy": "^1.1.7", - "semver": "^5.7.1", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.3", - "update-notifier": "^4.1.0" + "hash-base": "^3.1.2", + "inherits": "^2.0.4" }, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "hash-base": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz", + "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==", "dev": true, "requires": { - "ms": "^2.1.1" + "inherits": "^2.0.4", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.1" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } + } } } }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", - "dev": true, + "rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "requires": { - "abbrev": "1" + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "@types/estree": "1.0.8", + "fsevents": "~2.3.2" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-url": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", - "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==", - "dev": true - }, - "nth-check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", - "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "requires": { - "boolbase": "^1.0.0" + "queue-microtask": "^1.2.2" } }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" - } + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "requires": { - "wrappy": "1" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" } }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "sanitize-html": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz", + "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==", "dev": true, "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } } } }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" - }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "requires": { - "parse5": "^6.0.1" + "loose-envify": "^1.1.0" } }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", + "semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "dev": true }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "serve": { + "version": "14.2.6", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.6.tgz", + "integrity": "sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==", + "dev": true, "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" + "@zeit/schemas": "2.36.0", + "ajv": "8.18.0", + "arg": "5.0.2", + "boxen": "7.0.0", + "chalk": "5.0.1", + "chalk-template": "0.4.0", + "clipboardy": "3.0.0", + "compression": "1.8.1", + "is-port-reachable": "4.0.0", + "serve-handler": "6.1.7", + "update-check": "1.5.4" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "chalk": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.0.1.tgz", + "integrity": "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } } }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "serve-handler": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.7.tgz", + "integrity": "sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==", "dev": true, "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "bytes": "3.0.0", + "content-disposition": "0.5.2", + "mime-types": "2.1.18", + "minimatch": "3.1.5", + "path-is-inside": "1.0.2", + "path-to-regexp": "3.3.0", + "range-parser": "1.2.0" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true + } } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", + "set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "requires": { - "escape-goat": "^2.0.0" + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" } }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" - }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - } + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "dev": true, "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" } }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "requires": { - "picomatch": "^2.2.1" + "shebang-regex": "^3.0.0" } }, - "registry-auth-token": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz", - "integrity": "sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw==", + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "requires": { - "rc": "^1.2.8" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" } }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "requires": { - "rc": "^1.2.8" + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" } }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - } + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" } }, - "responselike": { + "side-channel-weakmap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, "requires": { - "lowercase-keys": "^1.0.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, "requires": { - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" } }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "requires": {} + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "dev": true + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" }, "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" } } }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" } }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" + "safe-buffer": "~5.2.0" } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + "string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" } }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^6.0.1" } }, + "strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true + }, "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "dev": true }, "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { - "has-flag": "^3.0.0" + "has-flag": "^4.0.0" } }, - "term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", + "tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==" + }, + "tailwindcss": { + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", + "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==" + }, + "tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==" + }, + "tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + } + }, + "terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "requires": { + "setimmediate": "^1.0.4" + } + }, + "tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "requires": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "dependencies": { + "fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" + } + } + }, + "to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -1586,184 +15280,334 @@ "is-number": "^7.0.0" } }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "requires": {} }, - "touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true + }, + "tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "requires": { - "nopt": "~1.0.10" + "prelude-ls": "^1.2.1" } }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "typescript": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", + "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==" + }, + "typescript-eslint": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-7.14.1.tgz", + "integrity": "sha512-Eo1X+Y0JgGPspcANKjeR6nIqXl4VL5ldXLc15k4m9upq+eY5fhU2IueiEZL6jmHrKH8aCfbIvM/v3IrX5Hg99w==", + "dev": true, "requires": { - "safe-buffer": "^5.0.1" + "@typescript-eslint/eslint-plugin": "7.14.1", + "@typescript-eslint/parser": "7.14.1", + "@typescript-eslint/utils": "7.14.1" } }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "dev": true }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "devOptional": true + }, + "update-check": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/update-check/-/update-check-1.5.4.tgz", + "integrity": "sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==", + "dev": true, "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0" } }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "requires": { - "is-typedarray": "^1.0.0" + "punycode": "^2.1.0" } }, - "undefsafe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.3.tgz", - "integrity": "sha512-nrXZwwXrD/T/JXeygJqdCO6NZZ1L66HrxM/Z7mIq2oPanoN0F1nLx3lwJMu6AwJY69hdixaFQOuoYsMjE5/C2A==", + "url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", "dev": true, "requires": { - "debug": "^2.2.0" + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + } } }, - "unique-string": { + "urlencode": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "resolved": "https://registry.npmjs.org/urlencode/-/urlencode-2.0.0.tgz", + "integrity": "sha512-K4+koEq4II9FqKKdLyMwfVFiWvTLJsdsIihXCprumjlOwpviO44E4hAhLYBLb6CEVTZh9hXXMTQHIT+Hwv5BPw==", "dev": true, "requires": { - "crypto-random-string": "^2.0.0" + "iconv-lite": "~0.6.3" } }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" - }, - "update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" + "use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "requires": { + "tslib": "^2.0.0" } }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", "requires": { - "punycode": "^2.1.0" + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" } }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "dev": true, + "requires": {} + }, + "util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", "dev": true, "requires": { - "prepend-http": "^2.0.0" + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" } }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "vite": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", + "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "fsevents": "~2.3.3", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "dependencies": { + "fdir": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", + "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "requires": {} + }, + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==" + } } }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "vite-plugin-node-polyfills": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", "dev": true, "requires": { - "string-width": "^4.0.0" + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.3.1" } }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", "dev": true }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "iconv-lite": "0.6.3" } }, - "xdg-basedir": { + "whatwg-mimetype": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + } + }, + "widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "dev": true, + "requires": { + "string-width": "^5.0.1" + }, + "dependencies": { + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + } + } + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true + }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true + }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==" + }, + "yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "devOptional": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + }, + "zustand": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.1.tgz", + "integrity": "sha512-pRET7Lao2z+n5R/HduXMio35TncTlSW68WsYBq2Lg1ASspsNGjpwLAsij3RpouyV6+kHMwwwzP0bZPD70/Jx/w==", + "dev": true, + "requires": {} } } } diff --git a/package.json b/package.json index 49487c8c3..fd1141596 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,98 @@ { - "name": "lnreader-backend", - "version": "1.0.0", - "description": "Backend for LNReader", - "main": "server.js", - "scripts": { - "start": "node server", - "server": "nodemon server" - }, - "author": "", - "license": "ISC", - "dependencies": { - "cheerio": "^1.0.0-rc.5", - "express": "^4.17.1", - "request": "^2.88.2" - }, - "devDependencies": { - "nodemon": "^2.0.6" - } + "name": "lnreader-plugins", + "version": "3.0.0", + "description": "Plugins repo for LNReader", + "main": "index.js", + "type": "module", + "scripts": { + "clean:multisrc": "find . -type f -wholename \"*plugins*\\[*\\]*.[t,j]s\" -delete", + "clean:multisrc:windows": "powershell -Command \"Get-ChildItem -Recurse -Include \\\"*].ts\\\",\\\"*].js\\\" | Where-Object { $_.FullName -like \\\"*plugins*\\\" } | Remove-Item\"", + "dev": "vite", + "dev:start": "node ./plugins/multisrc/generate-multisrc-plugins.js && node scripts/generate-plugin-index.js && vite", + "build:multisrc": "node ./plugins/multisrc/generate-multisrc-plugins.js", + "build:compile": "npx tsc --project tsconfig.production.json", + "build:manifest": "node ./scripts/build-plugin-manifest.js", + "build:manifest:dev": "node --env-file=.env ./scripts/build-plugin-manifest.js", + "build:icons": "npm run clean:multisrc && npm run build:multisrc && npm run build:compile && npm run build:manifest && node ./scripts/download-plugin-icons.js", + "build:full": "npm run clean:multisrc && npm run build:multisrc && npm run build:compile && npm run build:manifest", + "publish:plugins": "chmod +x ./scripts/publish-plugins.sh && ./scripts/publish-plugins.sh", + "publish:plugins:windows": "powershell ./scripts/publish-plugins.ps1", + "serve:dev": "npm run build:compile && npm run build:manifest:dev && serve", + "lint": "npx eslint .", + "lint:fix": "npx eslint . --fix", + "format": "prettier --write \"./**/*.{js,ts}\"", + "format:check": "prettier --check \"./**/*.{js,ts}\"", + "check:sites": "node scripts/check-plugin-sites.js", + "prepare": "husky" + }, + "author": "LNReader", + "license": "MIT", + "dependencies": { + "@fontsource/geist-mono": "^5.2.7", + "@fontsource/geist-sans": "^5.2.5", + "@noble/ciphers": "^2.1.1", + "@radix-ui/react-checkbox": "^1.3.3", + "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.1.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-tooltip": "^1.2.8", + "@tailwindcss/vite": "^4.1.14", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "jszip": "^3.10.1", + "lucide-react": "^0.546.0", + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.14", + "terser": "^5.36.0", + "typescript": "^5.7.2" + }, + "devDependencies": { + "@emotion/react": "^11.13.5", + "@emotion/styled": "^11.13.5", + "@eslint/js": "^9.5.0", + "@fontsource/roboto": "^5.1.0", + "@mui/icons-material": "^6.1.9", + "@mui/material": "^6.1.9", + "@reduxjs/toolkit": "^2.4.0", + "@types/eslint__js": "^8.42.3", + "@types/eslint-config-prettier": "^6.11.3", + "@types/http-proxy": "^1.17.16", + "@types/node": "^24.8.1", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@types/react-window": "^1.8.8", + "@vitejs/plugin-react-swc": "^3.9.0", + "cheerio": "^1.0.0", + "dayjs": "^1.11.13", + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "globals": "^15.6.0", + "htmlparser2": "^9.1.0", + "http-proxy": "^1.18.1", + "husky": "^9.0.11", + "image-size": "^1.1.1", + "lint-staged": "^15.2.2", + "prettier": "^3.2.5", + "protobufjs": "^7.4.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-redux": "^9.1.2", + "react-virtuoso": "^4.12.2", + "react-window": "^1.8.10", + "sanitize-html": "^2.13.1", + "serve": "^14.2.4", + "tw-animate-css": "^1.4.0", + "typescript-eslint": "^7.14.1", + "urlencode": "^2.0.0", + "vite": "^6.3.2", + "vite-plugin-node-polyfills": "^0.26.0", + "zustand": "^5.0.1" + }, + "lint-staged": { + "**/*.{js,ts,jsx,tsx}": "prettier --write" + } } diff --git a/plugins/arabic/.gitkeep b/plugins/arabic/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/arabic/dilartube.ts b/plugins/arabic/dilartube.ts new file mode 100644 index 000000000..f24158982 --- /dev/null +++ b/plugins/arabic/dilartube.ts @@ -0,0 +1,434 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class dilartube implements Plugin.PluginBase { + id = 'dilartube'; + name = 'dilar tube'; + version = '1.0.2'; + icon = 'src/ar/dilartube/icon.png'; + site = 'https://golden.rest/'; + + parseNovels(data: ApiResponse): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const seenTitles = new Set(); + if (data.data && data.data.length > 0) { + data.data + .filter(dataItem => dataItem.is_novel) + .forEach(dataItem => { + const manga = dataItem; + if (!seenTitles.has(dataItem.title)) { + seenTitles.add(manga.title); + novels.push({ + name: dataItem.title || 'novel', + path: `mangas/${manga.id}`, + cover: manga.cover + ? `${this.site}uploads/manga/cover/${manga.id}/${manga.cover}` + : defaultCover, + }); + } + }); + } + if (data.releases && data.releases.length > 0) { + data.releases + .filter(release => release.manga.is_novel) + .forEach(release => { + const manga = release.manga; + if (!seenTitles.has(manga.title)) { + seenTitles.add(manga.title); + novels.push({ + name: manga.title || 'novel', + path: `mangas/${manga.id}`, + cover: manga.cover + ? `${this.site}uploads/manga/cover/${manga.id}/${manga.cover}` + : defaultCover, + }); + } + }); + } + return novels; + } + + async popularNovels( + page: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `${this.site}api/releases?page=${page}`; + if (showLatestNovels) { + link = `${this.site}api/releases?page=${page}`; + } + // if (filters) { + // if ( + // Array.isArray(filters.categories.value) && + // filters.categories.value.length > 0 + // ) { + // filters.categories.value.forEach((genre: string) => { + // link += `&category=${genre}`; + // }); + // } + // if (filters.status.value !== '') { + // link += `&status=${filters.status.value}`; + // } + // } + // link += `?page=${page}`; + // const body = await fetchApi(link).then(r => r.text()); + const response = await fetchApi(link).then(r => r.json()); + return this.parseNovels(response); + } + + async parseNovel(novelUrl: string): Promise { + const chapterItems: Plugin.ChapterItem[] = []; + const fullUrl = this.site + 'api/' + novelUrl; + const chapterUrl = this.site + 'api/' + novelUrl + '/releases'; + const manga: MangaResponse = await fetchApi(fullUrl).then(r => r.json()); + const chapters = await fetchApi(chapterUrl).then(r => r.json()); + const mangaData = manga.mangaData; + const chapterData = chapters.releases; + + const novel: Plugin.SourceNovel = { + path: novelUrl, + name: mangaData.arabic_title ?? mangaData.title ?? 'Untitled', + author: + (mangaData.authors.length > 0 ? mangaData.authors[0].name : '') || + 'Unknown', + summary: mangaData.summary || '', + cover: `${this.site}uploads/manga/cover/${mangaData.id}/${mangaData.cover}`, + chapters: [], + }; + const translationStatusId = mangaData.translation_status.toString(); + const translationText = + { + '1': 'مستمره', + '0': 'منتهية', + '2': 'متوقفة', + '3': 'غير مترجمه', + }[translationStatusId] || 'غير معروف'; + // const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']); + const mainGenres = Array.from( + new Set(mangaData.categories.map(g => g.name)), + ) + .filter(Boolean) + .join(','); + novel.genres = `${translationText},${mainGenres}`; + + const statusId = mangaData.story_status.toString(); + const statusText = + { + '2': 'Ongoing', + '3': 'Completed', + }[statusId] || 'Unknown'; + novel.status = statusText; + chapterData.map((item: ChapterRelease) => { + chapterItems.push({ + name: item.title, + releaseTime: new Date(item.created_at).toISOString(), + path: `${novelUrl}/${mangaData.title.replace(' ', '-')}/${item.chapter}`, + chapterNumber: item.chapter, + }); + }); + novel.chapters = chapterItems.reverse(); + return novel; + } + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(new URL(chapterUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const jsonData = loadedCheerio('script.js-react-on-rails-component').html(); + const parsedData = JSON.parse(jsonData as string); + + const chapterText = parsedData.readerDataAction.readerData.release.content; + // return html with p tags + return chapterText + .split(/\r?\n/) + .map((line: string) => line.trim()) + .filter(Boolean) + .map((line: string) => `

${line}

`) + .join(''); + } + + async searchNovels( + searchTerm: string, + // page: number, + ): Promise { + const formData = new FormData(); + formData.append('query', searchTerm); + formData.append('includes', '["Manga","Team","Member"]'); + const response = await fetchApi('https://dilar.tube/api/quick_search', { + method: 'POST', + body: formData, + }).then(r => r.json()); + const data: ApiResponse = response[0]; + return this.parseNovels(data); + } + + filters: Filters | undefined = undefined; + // filters = { + // types: { + // value: [], + // label: 'الأنواع', + // options: [ + // { label: 'يابانية', value: '1' }, // Japanese + // { label: 'كورية', value: '2' }, // Korean + // { label: 'صينية', value: '3' }, // Chinese + // { label: 'عربية', value: '4' }, // Arabic + // { label: 'كوميك', value: '5' }, // Comic + // { label: 'هواة', value: '6' }, // Amateur + // { label: 'إندونيسية', value: '7' }, // Indonesian + // { label: 'روسية', value: '8' }, // Russian + // ], + // type: FilterTypes.CheckboxGroup, + // }, + + // status: { + // value: '', + // label: 'الحالة', + // options: [ + // { label: 'مستمرة', value: '2' }, // Ongoing + // { label: 'منتهية', value: '3' }, // Completed + // ], + // type: FilterTypes.CheckboxGroup, + // }, + + // translation: { + // value: [], + // label: 'الترجمة', + // options: [ + // { label: 'منتهية', value: '0' }, // Completed + // { label: 'مستمرة', value: '1' }, // Ongoing + // { label: 'متوقفة', value: '2' }, // Paused + // { label: 'غير مترجمة', value: '3' }, // Not Translated + // ], + // type: FilterTypes.CheckboxGroup, + // }, + // } satisfies Filters; +} + +export default new dilartube(); + +type Category = { + id: number; + name: string; + icon: string | null; + manga_id: number; +}; + +type Type = { + id: number; + name: string; + reading_direction: string; + title: string; +}; + +type Manga = { + id: number; + title: string; + summary: string; + is_novel: boolean; + is_oneshot: boolean; + genre_id: number; + cover: string; + cover_pos: number; + rectangle_cover_pos: number; + banner: string; + manga_type_id: number; + reading_direction: string; + story_status: number; + translation_status: number; + vols: number; + chaps: number; + reviewed: boolean; + banned: boolean; + rating: string; + rates_count: number; + commentable: boolean; + show_comments: boolean; + deleted_at: string | null; + delete_reason: string; + time_stamp: number; + latest_chapterization_id: number; + uniq_visitors_count: number; + publisher_id: number | null; + publisher_name: string | null; + discord_url: string | null; + mobile_exclusive: boolean; + // authors: any[]; + // artists: any[]; + categories: Category[]; + type: Type; +}; +type Release = { + id: number; + manga_id: number; + created_at: string; + time_stamp: number; + views: number; + encoded: boolean; + showable: boolean; + link_control: number; + support_link: string; + creator_id: number; + chapterization_id: number; + chapter: number; + volume: number; + title: string; + team_id: number; + team_name: string; + team_rating: string; + team_settings: string; + team_paypal: string | null; + has_rev_link: boolean; + manga: Manga; +}; +type ApiResponse = { + releases: Release[]; + data: searchManga[]; +}; +type MangaCategory = { + id: number; + name: string; + icon: string | null; + manga_id: number; +}; +type AuthorArtist = { + id: number; + name: string; + manga_id: number; + role: string; +}; + +type MangaType = { + id: number; + name: string; + reading_direction: string; + title: string; +}; + +type MangaData = { + id: number; + title: string; + summary: string; + is_novel: boolean; + is_oneshot: boolean; + genre_id: number; + cover: string; + cover_pos: number; + rectangle_cover_pos: number; + banner: string; + manga_type_id: number; + reading_direction: string; + story_status: number; + translation_status: number; + vols: number; + chaps: number; + reviewed: boolean; + banned: boolean; + rating: string; + rates_count: number; + commentable: boolean; + show_comments: boolean; + deleted_at: string | null; + delete_reason: string | null; + time_stamp: number; + latest_chapterization_id: number; + uniq_visitors_count: number; + publisher_id: number | null; + publisher_name: string | null; + arabic_title: string | null; + english: string; + synonyms: string; + japanese: string; + over17: boolean; + s_date: string; + e_date: string | null; + mal: string; + mangaupdates: string; + mangadex: string | null; + anilist: string; + official_site: string | null; + manga_fox: string | null; + wikia: string | null; + mangarock_oid: string | null; + comicvine_id: string | null; + external_source: string | null; + discord_url: string | null; + discord_id: string | null; + anime_start_end: string | null; + creator_id: number; + creator_nick: string; + editor_id: number; + editor_nick: string; + mobile_exclusive: boolean; + authors: AuthorArtist[]; + artists: AuthorArtist[]; + categories: MangaCategory[]; + type: MangaType; + memberRating: number; +}; + +type MangaResponse = { + // membersMentioning: any[]; + // memberRates: any | null; + // mangaLogs: Record; + mangaData: MangaData; +}; + +type ChapterRelease = { + id: number; + manga_id: number; + created_at: string; // ISO 8601 date string + time_stamp: number; + views: number; + link_control: number; + support_link: string; + init_team: number; + creator_id: number; + chapterization_id: number; + chapterization_time_stamp: number; + chapter: number; + volume: number; + title: string; + team_id: number; + team_name: string; + has_rev_link: boolean; +}; +type searchManga = { + // filter: any; + id: number; + title: string; + summary: string; + is_novel: boolean; + is_oneshot: boolean; + genre_id: number; + cover: string; + cover_pos: number; + rectangle_cover_pos: number; + banner: string; + manga_type_id: number; + reading_direction: string; + story_status: number; + translation_status: number; + vols: number; + chaps: number; + reviewed: boolean; + banned: boolean; + rating: string; + rates_count: number; + commentable: boolean; + show_comments: boolean; + deleted_at: string | null; + delete_reason: string; + time_stamp: number; + latest_chapterization_id: number; + uniq_visitors_count: number; + publisher_id: number | null; + publisher_name: string | null; + discord_url: string | null; + mobile_exclusive: boolean; + // authors: any[]; + // artists: any[]; + categories: Category[]; + type: Type; +}; diff --git a/plugins/arabic/rewayatclub.ts b/plugins/arabic/rewayatclub.ts new file mode 100644 index 000000000..025b43683 --- /dev/null +++ b/plugins/arabic/rewayatclub.ts @@ -0,0 +1,267 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class RewayatClub implements Plugin.PagePlugin { + id = 'rewayatclub'; + name = 'Rewayat Club'; + version = '1.0.3'; + icon = 'src/ar/rewayatclub/icon.png'; + site = 'https://rewayat.club/'; + + parseNovels(data: NovelData): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + if (data.results === undefined) return novels; + data.results.map((item: NovelEntry) => { + novels.push({ + name: item.arabic || item.novel?.arabic || 'novel', + path: item.slug + ? `novel/${item.slug}` + : item.novel + ? `novel/${item.novel.slug}` + : 'novel', + cover: item.poster_url + ? `https://api.rewayat.club/${item.poster_url.slice(1)}` + : item.novel + ? `https://api.rewayat.club/${item.novel.poster_url.slice(1)}` + : defaultCover, + }); + }); + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let link = `https://api.rewayat.club/api/novels/`; + let body: NovelData = { + count: 0, + next: '', + previous: '', + results: [], + }; + if (showLatestNovels) { + link = `${this.site}api/chapters/weekly/list/?page=${page}`; + body = await fetchApi(link).then(r => r.json()); + } else if (filters) { + if (filters.categories.value !== '') { + link += `?type=${filters.categories.value}`; + } + if (filters.sortOptions.value !== '') { + link += `&ordering=${filters.sortOptions.value}`; + } + if (filters.genre.value.length > 0) { + filters.genre.value.forEach((genre: string) => { + link += `&genre=${genre}`; + }); + } + link += `&page=${page}`; + body = await fetchApi(link).then(r => r.json()); + } + return this.parseNovels(body); + } + + async parseNovel( + novelUrl: string, + ): Promise { + const result = await fetchApi(new URL(novelUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelUrl, + name: loadedCheerio('h1.primary--text span').text().trim() || 'Untitled', + author: loadedCheerio('.novel-author').text().trim(), + summary: loadedCheerio('div.text-pre-line span').text().trim(), + totalPages: 1, + chapters: [], + }; + const statusWords = new Set(['مكتملة', 'متوقفة', 'مستمرة']); + const mainGenres = Array.from(loadedCheerio('.v-slide-group__content a')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const statusGenre = Array.from( + loadedCheerio('div.v-slide-group__content span.v-chip__content'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)); + novel.genres = `${statusGenre},${mainGenres}`; + const statusText = Array.from( + loadedCheerio('div.v-slide-group__content span.v-chip__content'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)) + .join(); + novel.status = + { + 'متوقفة': 'On Hiatus', + 'مكتملة': 'Completed', + 'مستمرة': 'Ongoing', + }[statusText] || 'Unknown'; + const imageRaw = loadedCheerio('body script:contains("__NUXT__")') + .first() + .text(); + const imageUrlRegex = /poster_url:"(\\u002F[^"]+)"/; + const imageUrlMatch = imageRaw?.match(imageUrlRegex); + const ImageUrlShort = imageUrlMatch + ? imageUrlMatch[1].replace(/\\u002F/g, '/').replace(/^\/*/, '') + : defaultCover; + const imageUrl = `https://api.rewayat.club/${ImageUrlShort}`; + novel.cover = imageUrl; + const chapterNumberStr = loadedCheerio('div.v-tab--active span.mr-1') + .text() + .replace(/[^\d]/g, ''); + const chapterNumber = parseInt(chapterNumberStr, 10); + const pageNumber = Math.ceil(chapterNumber / 24); + novel.totalPages = pageNumber; + + return novel; + } + parseChapters(data: ChapterData, novelPath: string) { + const chapter: Plugin.ChapterItem[] = []; + data.results.map((item: ChapterEntry) => { + chapter.push({ + name: item.title, + releaseTime: new Date(item.date).toISOString(), + path: `${novelPath}/${item.number}`, + chapterNumber: item.number, + }); + }); + return chapter; + } + async parsePage(novelPath: string, page: string): Promise { + const pagePath = novelPath.slice(6); + const pageUrl = `https://api.rewayat.club/api/chapters/${pagePath}/?ordering=number&page=${page}`; + const dataJson = await fetchApi(pageUrl).then(r => r.json()); + const chapters = this.parseChapters(dataJson, novelPath); + return { + chapters, + }; + } + async parseChapter(chapterUrl: string): Promise { + const link = this.site + 'api/chapters/' + chapterUrl.slice(6); + const result = await fetchApi(link).then(r => r.json()); + let chapterText = result.content + .flat() + .join('
') + .replace(/\n/g, '') + .replace(/

/g, '\n'); + chapterText = chapterText.trim(); + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `https://api.rewayat.club/api/novels/?type=0&ordering=-num_chapters&page=${page}&search=${searchTerm}`; + + const result = await fetchApi(searchUrl).then(r => r.json()); + return this.parseNovels(result); + } + + filters = { + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'كوميديا', value: '1' }, // Comedy + { label: 'أكشن', value: '2' }, // Action + { label: 'دراما', value: '3' }, // Drama + { label: 'فانتازيا', value: '4' }, // Fantasy + { label: 'مهارات القتال', value: '5' }, // Combat Skills + { label: 'مغامرة', value: '6' }, // Adventure + { label: 'رومانسي', value: '7' }, // Romance + { label: 'خيال علمي', value: '8' }, // Science Fiction + { label: 'الحياة المدرسية', value: '9' }, // School Life + { label: 'قوى خارقة', value: '10' }, // Super Powers + { label: 'سحر', value: '11' }, // Magic + { label: 'رياضة', value: '12' }, // Sports + { label: 'رعب', value: '13' }, // Horror + { label: 'حريم', value: '14' }, // Harem + ], + type: FilterTypes.CheckboxGroup, + }, + categories: { + value: '0', + label: 'الفئات', + options: [ + { label: 'جميع الروايات', value: '0' }, + { label: 'مترجمة', value: '1' }, + { label: 'مؤلفة', value: '2' }, + { label: 'مكتملة', value: '3' }, + ], + type: FilterTypes.Picker, + }, + sortOptions: { + value: '-num_chapters', + label: 'الترتيب', + options: [ + { label: 'عدد الفصول - من أقل ﻷعلى', value: 'num_chapters' }, + { label: 'عدد الفصول - من أعلى ﻷقل', value: '-num_chapters' }, + { label: 'الاسم - من أقل ﻷعلى', value: 'english' }, + { label: 'الاسم - من أعلى ﻷقل', value: '-english' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RewayatClub(); + +type NovelEntry = { + arabic: string; + english: string; + about: string; + poster_url: string; + slug: string; + original: boolean; + complete: boolean; + num_chapters: number; + genre: { + id: number; + arabic: string; + english: string; + }; + novel?: { + arabic: string; + english: string; + slug: string; + poster: string; + id: number; + poster_url: string; + original: boolean; + }; +}; +type NovelData = { + count?: number; + next?: string; + previous?: string; + results: NovelEntry[]; +}; +type ChapterEntry = { + number: number; + title: string; + date: string; + uploader: { + username: string; + id: number; + }; + hitcounts: { + hits: number; + id: number; + }; + // read: any[]; +}; + +type ChapterData = { + count: number; + next: string; + previous: string; + results: ChapterEntry[]; +}; diff --git a/plugins/arabic/sunovels.ts b/plugins/arabic/sunovels.ts new file mode 100644 index 000000000..d7e34063d --- /dev/null +++ b/plugins/arabic/sunovels.ts @@ -0,0 +1,305 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class Sunovels implements Plugin.PagePlugin { + id = 'sunovels'; + name = 'Sunovels'; + version = '1.0.1'; + icon = 'src/ar/sunovels/icon.png'; + site = 'https://sunovels.com/'; + + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const imageUrlList: string[] = []; + loadedCheerio('script').each((idx, ele) => { + const regax = /\/uploads\/[^\s"]+/g; + const scriptText = loadedCheerio(ele).text(); + const imageUrlMatched = scriptText.match(regax); + if (imageUrlMatched) { + imageUrlList.push(...imageUrlMatched); + } + }); + let counter = 0; + loadedCheerio('.list-item').each((idx, ele) => { + loadedCheerio(ele) + .find('a') + .each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h4').text().trim(); + const novelUrl = + loadedCheerio(ele).attr('href')?.trim().replace(/^\/*/, '') || ''; + let novelCover = defaultCover; + if (imageUrlList.length > 0) { + novelCover = this.site + imageUrlList[counter].slice(1); + } else { + const imageUrl = loadedCheerio(ele).find('img').attr('src'); + novelCover = this.site + imageUrl?.slice(1); + } + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; + counter++; + novels.push(novel); + }); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const pageCorrected = page - 1; + let link = `${this.site}library?`; + + if (filters) { + if ( + Array.isArray(filters.categories.value) && + filters.categories.value.length > 0 + ) { + filters.categories.value.forEach((genre: string) => { + link += `&category=${genre}`; + }); + } + if (filters.status.value !== '') { + link += `&status=${filters.status.value}`; + } + } + link += `&page=${pageCorrected}`; + const body = await fetchApi(link).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel( + novelUrl: string, + ): Promise { + const result = await fetchApi(new URL(novelUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelUrl, + name: loadedCheerio('div.main-head h3').text().trim() || 'Untitled', + author: loadedCheerio('.novel-author').text().trim(), + summary: loadedCheerio('section.info-section div.description p') + .text() + .trim(), + totalPages: 1, + chapters: [], + }; + const statusWords = new Set(['مكتمل', 'جديد', 'مستمر']); + const mainGenres = Array.from(loadedCheerio('div.categories li.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const statusGenre = Array.from( + loadedCheerio('div.header-stats span').eq(3).find('strong'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)); + novel.genres = `${statusGenre},${mainGenres}`; + const statusText = Array.from( + loadedCheerio('div.header-stats span').eq(3).find('strong'), + ) + .map(el => loadedCheerio(el).text().trim()) + .filter(text => statusWords.has(text)) + .join(); + novel.status = + { + 'جديد': 'Ongoing', + 'مكتمل': 'Completed', + 'مستمر': 'Ongoing', + }[statusText] || 'Unknown'; + const imageUrl = loadedCheerio('div.img-container figure.cover img').attr( + 'src', + ); + const imageUrlFull = this.site + imageUrl?.slice(1); + novel.cover = imageUrlFull; + const chapterNumberStr = loadedCheerio('div.header-stats span') + .first() + .text() + .replace(/[^\d]/g, ''); + const chapterNumber = parseInt(chapterNumberStr, 10); + const pageNumber = Math.ceil(chapterNumber / 50); + novel.totalPages = pageNumber; + + return novel; + } + parseChapters(data: { chapters: ChapterEntry[] }) { + const chapter: Plugin.ChapterItem[] = []; + data.chapters.map((item: ChapterEntry) => { + chapter.push({ + name: item.chapterName, + releaseTime: new Date(item.releaseTime).toISOString(), + path: item.chapterUrl, + chapterNumber: Number(item.chapterNumber), + }); + }); + return chapter; + } + async parsePage(novelPath: string, page: string): Promise { + const numPage = parseInt(page, 10); + const pageCorrected = numPage - 1; + const pagePath = novelPath; + const firstUrl = this.site + pagePath; + const pageUrl = firstUrl + '?activeTab=chapters&page=' + pageCorrected; + const body = await fetchApi(pageUrl).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const dataJson: { + pages_count: string; + chapters: ChapterEntry[]; + } = { pages_count: '', chapters: [] }; + const chaptersinfo: { + chapterName: string; + chapterUrl: string; + releaseTime: string; + chapterNumber: string | number; + }[] = []; + loadedCheerio('ul.chaptersList a').each((i, el) => { + const chapterName: string = loadedCheerio(el).attr('title') ?? ''; + const chapterUrl = loadedCheerio(el) + .attr('href') + ?.trim() + .replace(/^\/*/, ''); + const dateAttr = loadedCheerio(el) + .find('time.chapter-update') + .attr('datetime'); + const releaseTime = dateAttr ? new Date(dateAttr).toISOString() : ''; + const chapternumber = loadedCheerio(el) + .find('strong.chapter-title') + .text() + .replace(/[^\d٠-٩]/g, ''); + const chapterNumber = parseInt(chapternumber, 10); + chaptersinfo.push({ + chapterName: chapterName, + chapterUrl: chapterUrl || '', + releaseTime: releaseTime || '', + chapterNumber: chapterNumber || '', + }); + }); + const pagecount = loadedCheerio('ul.pagination a.active').text(); + dataJson.pages_count = pagecount; + + dataJson.chapters = chaptersinfo; + const chapters = this.parseChapters(dataJson); + return { + chapters, + }; + } + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(new URL(chapterUrl, this.site).toString()); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + let chapterText = ''; + loadedCheerio('div.chapter-content').each((idx, ele) => { + loadedCheerio(ele) + .find('p') + .not('.d-none') + .each((idx, textEle) => { + chapterText += + loadedCheerio(textEle) + .map((_, pEle) => loadedCheerio(pEle).text().trim()) + .get() + .join(' ') + ' '; + }); + }); + chapterText = chapterText.trim(); + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const searchUrl = `${this.site}search?page=${page}&title=${searchTerm}`; + + const result = await fetchApi(searchUrl); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + categories: { + value: [], + label: 'التصنيفات', + options: [ + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'XUANHUAN', value: 'XUANHUAN' }, + { label: 'أصلية', value: 'أصلية' }, // Original + { label: 'أكشن', value: 'أكشن' }, // Action + { label: 'إثارة', value: 'إثارة' }, // Thriller + { label: 'إنتقال الى عالم أخر', value: 'إنتقال+الى+عالم+أخر' }, // Isekai + { label: 'إيتشي', value: 'إيتشي' }, // Ecchi + { label: 'الخيال العلمي', value: 'الخيال+العلمي' }, // Science Fiction + { label: 'بوليسي', value: 'بوليسي' }, // Detective + { label: 'تاريخي', value: 'تاريخي' }, // Historical + { label: 'تقمص شخصيات', value: 'تقمص+شخصيات' }, // Roleplaying + { label: 'جريمة', value: 'جريمة' }, // Crime + { label: 'جوسى', value: 'جوسى' }, // Josei + { label: 'حريم', value: 'حريم' }, // Harem + { label: 'حياة مدرسية', value: 'حياة+مدرسية' }, // School Life + { label: 'خارقة للطبيعة', value: 'خارقة+للطبيعة' }, // Supernatural + { label: 'خيالي', value: 'خيالي' }, // Fantasy + { label: 'دراما', value: 'دراما' }, // Drama + { label: 'رعب', value: 'رعب' }, // Horror + { label: 'رومانسي', value: 'رومانسي' }, // Romance + { label: 'سحر', value: 'سحر' }, // Magic + { label: 'سينن', value: 'سينن' }, // Seinen + { label: 'شريحة من الحياة', value: 'شريحة+من+الحياة' }, // Slice of Life + { label: 'شونين', value: 'شونين' }, // Shounen + { label: 'غموض', value: 'غموض' }, // Mystery + { label: 'فنون القتال', value: 'فنون+القتال' }, // Martial Arts + { label: 'قوى خارقة', value: 'قوى+خارقة' }, // Super Powers + { label: 'كوميدى', value: 'كوميدى' }, // Comedy + { label: 'مأساوي', value: 'مأساوي' }, // Tragedy + { label: 'ما بعد الكارثة', value: 'ما+بعد+الكارثة' }, // Post-Apocalypse + { label: 'مغامرة', value: 'مغامرة' }, // Adventure + { label: 'ميكا', value: 'ميكا' }, // Mecha + { label: 'ناضج', value: 'ناضج' }, // Mature + { label: 'نفسي', value: 'نفسي' }, // Psychological + { label: 'فانتازيا', value: 'فانتازيا' }, // Fantasy + { label: 'رياضة', value: 'رياضة' }, // Sports + { label: 'ابراج', value: 'ابراج' }, // Astrology + { label: 'الالهة', value: 'الالهة' }, // Deities + { label: 'شياطين', value: 'شياطين' }, // Demons + { label: 'السفر عبر الزمن', value: 'السفر+عبر+الزمن' }, // Time Travel + { label: 'رواية صينية', value: 'رواية+صينية' }, // Chinese Novel + { label: 'رواية ويب', value: 'رواية+ويب' }, // Web Novel + { label: 'لايت نوفل', value: 'لايت+نوفل' }, // Light Novel + { label: 'كوري', value: 'كوري' }, // Korean + { label: '+18', value: '%2B18' }, // +18 + { label: 'إيسكاي', value: 'إيسكاي' }, // Isekai + { label: 'ياباني', value: 'ياباني' }, // Japanese + { label: 'مؤلفة', value: 'مؤلفة' }, // Authored + ], + type: FilterTypes.CheckboxGroup, + }, + status: { + value: '', + label: 'الحالة', + options: [ + { label: 'جميع الروايات', value: '' }, + { label: 'مكتمل', value: 'Completed' }, + { label: 'جديد', value: 'New' }, + { label: 'مستمر', value: 'Ongoing' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Sunovels(); + +type ChapterEntry = { + chapterName: string; + chapterUrl: string; + releaseTime: string; + chapterNumber: string | number; +}; diff --git a/plugins/chinese/69shu.ts b/plugins/chinese/69shu.ts new file mode 100644 index 000000000..82382cddc --- /dev/null +++ b/plugins/chinese/69shu.ts @@ -0,0 +1,297 @@ +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'; + +class Shu69 implements Plugin.PluginBase { + private fetchOptions = { + headers: { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:137.0) Gecko/20100101 Firefox/137.0', + 'Accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,us;q=0.5', + 'Referer': 'https://www.69shu.xyz/', // Referer + 'DNT': '1', // Do Not Track + 'Upgrade-Insecure-Requests': '1', // Upgrade-Insecure-Requests + }, + }; + + id = '69shu'; + name = '69书吧'; + icon = 'src/cn/69shu/icon.png'; + site = 'https://www.69shu.xyz'; + version = '0.2.2'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + let url: string; + if (showLatestNovels) { + url = `${this.site}/rank/lastupdate/${pageNo}.html`; + } else if (filters.sort.value === 'none') { + url = `${this.site}/rank/${filters.rank.value}/${pageNo}.html`; + } else { + url = `${this.site}/sort/${filters.sort.value}/${pageNo}.html`; + } + + const body = await fetchText(url, this.fetchOptions); + if (body === '') throw Error('无法获取小说列表,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.book-coverlist').each((i, el) => { + const url = loadedCheerio(el).find('a.cover').attr('href'); + + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + path: url.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + + const body = await fetchText(url, this.fetchOptions); + if (body === '') throw Error('无法获取小说内容,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + chapters: [], + name: loadedCheerio('h1').text().trim(), + }; + + novel.cover = loadedCheerio('div.cover > img').attr('src'); + + novel.summary = loadedCheerio('#bookIntro').text().trim(); + + const bookInfo = loadedCheerio('div.caption-bookinfo > p'); + + novel.author = bookInfo.find('a').attr('title'); + + novel.artist = undefined; + + novel.status = bookInfo.text().includes('连载') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + + novel.genres = ''; + + // Table of Content is on a different page than the summary page + const chapters: Plugin.ChapterItem[] = []; + + const allUrl = loadedCheerio('dd.all > a').attr('href'); + if (allUrl) { + // --- Start: Fetch chapters with pagination (Sequential) --- + let currentChaptersUrl = new URL(allUrl, this.site).toString(); + let hasMorePages = true; + + while (hasMorePages) { + const chaptersBody = await fetchText( + currentChaptersUrl, + this.fetchOptions, + ); + const chaptersLoadedCheerio = parseHTML(chaptersBody); + + // Extract chapters from the current page + chaptersLoadedCheerio('dl.panel-chapterlist dd').each((i, el) => { + const chapterUrl = chaptersLoadedCheerio(el).find('a').attr('href'); + const chapterName = chaptersLoadedCheerio(el).find('a').text().trim(); + if (chapterUrl) { + // Ensure relative path, handle both absolute/relative cases + const relativeChapterUrl = chapterUrl.startsWith('http') + ? chapterUrl.replace(this.site, '') + : chapterUrl; + // Avoid duplicates if the same chapter appears on multiple pages (unlikely but safe) + if (!chapters.some(chap => chap.path === relativeChapterUrl)) { + chapters.push({ + name: chapterName, + path: relativeChapterUrl, + }); + } + } + }); + + // Find the link to the next page using the text "下一页" + const nextPageLinkElement = chaptersLoadedCheerio( + 'div.listpage a.onclick', + ).filter((i, el) => + chaptersLoadedCheerio(el).text().includes('下一页'), + ); + const nextPageLink = nextPageLinkElement.attr('href'); + + if (nextPageLink && nextPageLink !== 'javascript:void(0);') { + // Check if it's a valid relative or absolute URL before creating the URL object + try { + const absoluteNextPageUrl = new URL( + nextPageLink, + this.site, + ).toString(); + if (absoluteNextPageUrl === currentChaptersUrl) { + // Break if the next page URL is the same as the current one (prevents infinite loops) + hasMorePages = false; + } else { + currentChaptersUrl = absoluteNextPageUrl; + } + } catch (e) { + // Handle cases where the link might be invalid or unexpected + console.warn(`Invalid next page link found: ${nextPageLink}`); + hasMorePages = false; + } + } else { + hasMorePages = false; + } + } + // --- End: Fetch chapters with pagination (Sequential) --- + } else { + // Fallback if no "all chapters" link is found + loadedCheerio( + 'div.panel.hidden-xs > dl.panel-chapterlist:nth-child(2) > dd', + ).each((i, el) => { + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + const chapterName = loadedCheerio(el).find('a').text().trim(); + if (chapterUrl) { + const relativeChapterUrl = chapterUrl.startsWith('http') + ? chapterUrl.replace(this.site, '') + : chapterUrl; + chapters.push({ + name: chapterName, + path: relativeChapterUrl, + }); + } + }); + } + + // Remove duplicates just in case (though less likely with sequential fetching) + const uniqueChapters = chapters.filter( + (chapter, index, self) => + index === self.findIndex(c => c.path === chapter.path), + ); + + novel.chapters = uniqueChapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const chapterUrl = new URL(chapterPath, this.site).toString(); + const body = await fetchText(chapterUrl, this.fetchOptions); // Header hinzugefügt + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chaptercontent p') + .map((i, el) => loadedCheerio(el).text()) + .get() + // remove empty lines and 69shu ads + .map((line: string) => line.trim()) + .filter((line: string) => line !== '' && !line.includes('69书吧')) + .map((line: string) => `

${line}

`) + .join('\n'); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + + const searchUrl = `${this.site}/search`; + const formData = new FormData(); + formData.append('searchkey', searchTerm); + + const searchOptions = { + ...this.fetchOptions, + method: 'post', + body: formData, + headers: { + ...this.fetchOptions.headers, + }, + }; + + const body = await fetchText(searchUrl, searchOptions); + if (body === '') throw Error('无法获取搜索结果,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.book-coverlist').each((i, el) => { + const url = loadedCheerio(el).find('a.cover').attr('href'); + + const novelName = loadedCheerio(el).find('h4.name').text().trim(); + const novelCover = loadedCheerio(el).find('a.cover > img').attr('src'); + + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + path: url.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + filters = { + rank: { + label: '排行榜', + value: 'allvisit', + options: [ + { label: '总排行榜', value: 'allvisit' }, + { label: '月排行榜', value: 'monthvisit' }, + { label: '周排行榜', value: 'weekvisit' }, + { label: '日排行榜', value: 'dayvisit' }, + { label: '收藏榜', value: 'goodnum' }, + { label: '字数榜', value: 'words' }, + { label: '推荐榜', value: 'allvote' }, + { label: '新书榜', value: 'postdate' }, + { label: '更新榜', value: 'lastupdate' }, + ], + type: FilterTypes.Picker, + }, + sort: { + label: '分类', + value: 'none', + options: [ + { label: '无', value: 'none' }, + { label: '全部', value: 'all' }, + { label: '玄幻', value: 'xuanhuan' }, + { label: '仙侠', value: 'xianxia' }, + { label: '都市', value: 'dushi' }, + { label: '历史', value: 'lishi' }, + { label: '游戏', value: 'youxi' }, + { label: '科幻', value: 'kehuan' }, + { label: '灵异', value: 'kongbu' }, + { label: '言情', value: 'nvsheng' }, + { label: '其它', value: 'qita' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Shu69(); diff --git a/plugins/chinese/Quanben.ts b/plugins/chinese/Quanben.ts new file mode 100644 index 000000000..a98a4d99d --- /dev/null +++ b/plugins/chinese/Quanben.ts @@ -0,0 +1,285 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; + +const parseUrl = (url?: string): URL | undefined => { + if (!url) return undefined; + try { + return new URL(url, 'https://www.quanben.io'); + } catch { + return undefined; + } +}; + +const getStandardNovelPath = (url?: string): string | undefined => { + const parsedUrl = parseUrl(url); + if (!parsedUrl) return undefined; + const match = parsedUrl.pathname.match(/^(\/amp)?(\/n\/[^/]+\/)/); + return match?.[2]?.replace(/^\//, ''); +}; + +// const getChapterFileName = (url?: string): string | undefined => { +// const parsedUrl = parseUrl(url); +// if (!parsedUrl) return undefined; +// const fileName = parsedUrl.pathname.split('/').pop(); +// if (fileName && /^\d+\.html$/.test(fileName)) return fileName; +// return undefined; +// }; + +const makeAbsolute = ( + relativeUrl?: string, + baseUrl?: string, +): string | undefined => { + if (!relativeUrl || !baseUrl) return undefined; + try { + if (relativeUrl.startsWith('//')) return 'https:' + relativeUrl; + if (/^https?:\/\//.test(relativeUrl)) return relativeUrl; + return new URL(relativeUrl, baseUrl).href; + } catch { + return undefined; + } +}; + +class QuanbenPlugin implements Plugin.PluginBase { + id = 'quanben'; + name = 'Quanben'; + site = 'https://www.quanben.io/'; + version = '1.1.1'; + icon = 'src/cn/quanben/icon.png'; + defaultCover = defaultCover; + + // filters + filters = { + genre: { + label: '分类', + value: 'all', + options: [ + { label: '全部', value: 'all' }, + { label: '玄幻', value: 'xuanhuan' }, + { label: '都市', value: 'dushi' }, + { label: '言情', value: 'yanqing' }, + { label: '穿越', value: 'chuanyue' }, + { label: '青春', value: 'qingchun' }, + { label: '仙侠', value: 'xianxia' }, + { label: '灵异', value: 'lingyi' }, + { label: '悬疑', value: 'xuanyi' }, + { label: '历史', value: 'lishi' }, + { label: '军事', value: 'junshi' }, + { label: '游戏', value: 'youxi' }, + { label: '竞技', value: 'jingji' }, + { label: '科幻', value: 'kehuan' }, + { label: '职场', value: 'zhichang' }, + { label: '官场', value: 'guanchang' }, + { label: '现言', value: 'xianyan' }, + { label: '耽美', value: 'danmei' }, + { label: '其它', value: 'qita' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; + + // homepage, when you first open the extension (with the applied filters if any) + async popularNovels( + _pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const url = + filters.genre.value === 'all' + ? this.site + : `${this.site}c/${filters.genre.value}.html`; + + const res = await fetchApi(url); + if (!res.ok) + throw new Error(`[Quanben] Failed to fetch: ${url} - ${res.status}`); + + const $ = parseHTML(await res.text()); + const novels: Plugin.NovelItem[] = []; + + $('div.list2').each((_i, list2) => { + const $list2 = $(list2); + const $link = $list2.find('h3 > a').first(); + const href = $link.attr('href')?.trim(); + const name = $link.text().trim(); + const rawCover = + $list2.find('img').attr('src')?.trim() || + $list2.find('img').attr('data-src')?.trim(); + const cover = makeAbsolute(rawCover, this.site) || this.defaultCover; + + if (href && name) { + const path = getStandardNovelPath(href); + if (path) novels.push({ name, path, cover }); + } + }); + + // only first entry bcs the others dont have an image + $('ul.list').each((_i, ul) => { + const $firstLi = $(ul).find('li').first(); + const $a = $firstLi.find('a').first(); + const href = $a.attr('href')?.trim(); + const name = + $a.text().trim() || $firstLi.find('span.author').text().trim(); + const cover = this.defaultCover; + + if (href && name) { + const path = getStandardNovelPath(href); + if (path) novels.push({ name, path, cover }); + } + }); + + return novels; + } + + // novel details and metadata + async parseNovel(novelPath: string): Promise { + const standardPath = novelPath.replace(/^\/amp/, '').replace(/^\//, ''); + if (!standardPath.startsWith('n/') || !standardPath.endsWith('/')) + throw new Error(`[Quanben parseNovel] Invalid path: ${novelPath}`); + + const fullUrl = this.site + standardPath; + + const res = await fetchApi(fullUrl); + if (!res.ok) + throw new Error(`[Quanben parseNovel] Failed to fetch: ${fullUrl}`); + + const $ = parseHTML(await res.text()); + + // Helper to read Open Graph / novel meta tags, falling back to empty string + const getMeta = (prop: string) => + $(`meta[property="${prop}"]`).attr('content')?.trim() || ''; + + const $info = $('div.list2').first(); + const $desc = $('div.description').first(); + + const statusText = getMeta('og:novel:status'); + + const novel: Plugin.SourceNovel = { + path: standardPath, + name: + getMeta('og:novel:book_name') || + $info.find('h3').text().trim() || + 'Unknown Novel', + cover: + getMeta('og:image') || + makeAbsolute($info.find('img').attr('src'), this.site) || + this.defaultCover, + summary: + getMeta('og:description') || + $desc.find('p').text().trim() || + $desc.text().trim() || + undefined, + author: + getMeta('og:novel:author') || + $info.find("p:contains('作者:') span").text().trim() || + undefined, + status: statusText + ? statusText.includes('完结') + ? NovelStatus.Completed + : NovelStatus.Ongoing + : NovelStatus.Unknown, + genres: + getMeta('og:novel:category') || + $info.find("p:contains('类别:') span").text().trim() || + undefined, + chapters: await this.parseChapterList(standardPath), + }; + + return novel; + } + + async parseChapterList(novelPath: string): Promise { + if (!novelPath.startsWith('n/') || !novelPath.endsWith('/')) return []; + + const novelSlug = novelPath.match(/^n\/([^/]+)\//)?.[1]; + if (!novelSlug) return []; + + const mirrorUrl = `https://quanben5.com/n/${novelSlug}/xiaoshuo.html`; + const res = await fetchApi(mirrorUrl); + if (!res.ok) return []; + + const $ = parseHTML(await res.text()); + const chapters: Plugin.ChapterItem[] = []; + + $('ul li a').each((_, el) => { + const name = $(el).text().trim(); + if (!name) return; + const i = chapters.length + 1; + chapters.push({ + name, + path: `${novelSlug}/${i}.html`, + chapterNumber: i, + }); + }); + + return chapters; + } + + async parseChapter(chapterPath: string): Promise { + if (!chapterPath.includes('/') || chapterPath.endsWith('/')) + throw new Error(`[Quanben] Invalid chapter path: "${chapterPath}"`); + + const url = `${this.site}n/${chapterPath}`; + const res = await fetchApi(url); + if (!res.ok) throw new Error(`[Quanben] Failed to fetch chapter: ${url}`); + + return this.extractChapterContent(await res.text()); + } + + // Helper function to extract and clean chapter content from HTML body + private extractChapterContent(body: string): string { + const $ = parseHTML(body); + const $content = $('#contentbody, #content, .content').first(); + if (!$content.length) return 'Error: Chapter content not found.'; + + $content + .find( + 'script, style, ins, iframe, [class*="ads"], [id*="ads"], [class*="google"], [id*="google"], [class*="recommend"], div[align="center"]', + ) + .remove(); + + return ( + ($content.html() || '').replace(/[\t ]+/g, ' ').trim() || + 'Error: Chapter content empty.' + ); + } + + // add search + async searchNovels(searchTerm: string): Promise { + const url = `${this.site}index.php?c=book&a=search&keywords=${encodeURIComponent(searchTerm)}`; + const res = await fetchApi(url); + if (!res.ok) return []; + + const $ = parseHTML(await res.text()); + const novels: Plugin.NovelItem[] = []; + + $('div.list2').each((_i, el) => { + const $el = $(el); + const $link = $el.find('h3 > a').first(); + const href = $link.attr('href'); + const name = $link.text().trim(); + const cover = makeAbsolute( + $el.find('img').attr('src') || $el.find('img').attr('data-src'), + this.site, + ); + + if (href && name) { + const path = getStandardNovelPath(makeAbsolute(href, this.site)); + if (path) + novels.push({ + name, + path, + cover: cover || this.defaultCover, + }); + } + }); + return novels; + } + + async fetchImage(url: string): Promise { + return fetchApi(url); + } +} + +export default new QuanbenPlugin(); diff --git a/plugins/chinese/ixdzs8.ts b/plugins/chinese/ixdzs8.ts new file mode 100644 index 000000000..b9539f6ef --- /dev/null +++ b/plugins/chinese/ixdzs8.ts @@ -0,0 +1,313 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +const makeAbsolute = ( + relativeUrl: string | undefined, + baseUrl: string, +): string | undefined => { + if (!relativeUrl) return undefined; + try { + if (relativeUrl.startsWith('//')) { + return new URL(baseUrl).protocol + relativeUrl; + } + if ( + relativeUrl.startsWith('http://') || + relativeUrl.startsWith('https://') + ) { + return relativeUrl; + } + return new URL(relativeUrl, baseUrl).href; + } catch { + return undefined; + } +}; + +class ixdzs8Plugin implements Plugin.PluginBase { + id = 'ixdzs8'; + name = '爱下电子书'; + site = 'https://ixdzs8.com/'; + version = '2.2.9'; + icon = 'src/cn/ixdzs8/favicon.png'; + + imageRequestInit = { + headers: { + Referer: this.site, + }, + }; + + async popularNovels(pageNo: number): Promise { + const url = `${this.site}hot/?page=${pageNo}`; + const result = await fetchApi(url); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const novels: Plugin.NovelItem[] = []; + const processedPaths = new Set(); + + $('ul.u-list > li.burl').each((_i, el) => { + const $el = $(el); + + const $link = $el.find('.l-info h3 a'); + const novelPath: string | undefined = $link.attr('href')?.trim(); + const novelName: string | undefined = ( + $link.attr('title') || + $link.text() || + '' + ).trim(); + const novelCover: string | undefined = $el + .find('.l-img img') + .attr('src') + ?.trim(); + + if (novelPath && novelName) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + processedPaths.add(novelPath); + } + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const novelUrl = makeAbsolute(novelPath, this.site); + if (!novelUrl) throw new Error('Invalid novel URL'); + + const result = await fetchApi(novelUrl); + if (!result.ok) throw new Error('Failed to fetch novel'); + + const $ = parseHTML(await result.text()); + const $novel = $('div.novel'); + const $intro = $('p#intro.pintro'); + // Remove any child elements you don't want, e.g., the "read more" icon + $intro.find('span.icon').remove(); + // Get text and normalize whitespace + const summary = $intro + .html() // get inner HTML + ?.replace(//gi, '\n') // convert
to line breaks + .replace(/<[^>]+>/g, '') // remove remaining tags like + .replace(/\u3000/g, ' ') // replace full-width spaces + .replace(/ /g, ' ') // replace spaces + .split('\n') // split into lines + .map(line => line.trim()) // trim each line + .filter(line => line.length > 0) // remove empty lines + .join(' '); // join into one paragraph + + const $tagsDiv = $('div.panel div.tags'); + // find all inside and get their text + // Select

that contains + + const statOngoing = $novel.find('.n-text p span.lz').text().trim(); + const statEnd = $novel.find('.n-text p span.end').text().trim(); + let detail: 'Ongoing' | 'Completed' | 'Unknown' = 'Unknown'; + if (statEnd.length > 0) { + detail = 'Completed'; + } else if (statOngoing.length > 0) { + detail = 'Ongoing'; + } else { + detail = 'Unknown'; + } + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $novel.find('.n-text h1').text().trim() || 'Untitled', + cover: + makeAbsolute($novel.find('.n-img img').attr('src'), this.site) || + defaultCover, + summary: summary, + author: $novel.find('.n-text p a.bauthor').text().trim() || undefined, + genres: $tagsDiv + .find('em a') + .map((_i, el) => $(el).text().trim()) + .get() + .filter(tag => tag.length > 0) // remove empty strings + .join(', '), + status: + detail === 'Ongoing' + ? NovelStatus.Ongoing + : detail === 'Completed' + ? NovelStatus.Completed + : NovelStatus.Unknown, + chapters: [], + }; + + const chapterListPath = $('#bid').attr('value'); // get bid from page + if (chapterListPath) { + novel.chapters = await this.parseChapterList(chapterListPath); + } + + return novel; + } + + async parseChapterList(bid: string | number): Promise { + // Convert bid to string just in case + const bookId = String(bid); + + // POST request to fetch chapter list JSON + const url = `${this.site}novel/clist/`; + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: `bid=${bookId}`, + }); + + if (!res.ok) { + throw new Error(`Failed to fetch chapters for bid=${bookId}`); + } + + const json: ChapterJSON = await res.json(); + + if (json.rs !== 200 || !Array.isArray(json.data)) { + throw new Error('Invalid response format from chapter list'); + } + + // Build chapters array + const chapters: Plugin.ChapterItem[] = json.data.map(ch => { + return { + name: ch.title, + path: ch.ctype === '0' ? `read/${bookId}/p${ch.ordernum}.html` : '', // only normal chapters get link + releaseTime: undefined, // optional, not provided here + }; + }); + + return chapters; + } + + async parseChapter(chapterPath: string): Promise { + const chapterUrl = makeAbsolute(chapterPath, this.site); + if (!chapterUrl) throw new Error('Invalid chapter URL'); + + // --- 1st request --- + let result = await fetchApi(chapterUrl); + if (!result.ok) throw new Error(`Failed to fetch chapter at ${chapterUrl}`); + + let html = await result.text(); + + // --- Check if we got challenge page --- + if (html.includes('正在進行安全驗證') || html.includes('challenge')) { + const tokenMatch = html.match(/let token\s*=\s*"([^"]+)"/); + if (tokenMatch) { + const challengeUrl = + chapterUrl + '?challenge=' + encodeURIComponent(tokenMatch[1]); + + result = await fetchApi(challengeUrl); + if (!result.ok) + throw new Error(`Failed after challenge redirect: ${challengeUrl}`); + html = await result.text(); + } + } + + // --- Parse content --- + const $ = parseHTML(html); + const $content = $('article section'); + + if (!$content.length) { + return `Error: Could not find chapter content at ${chapterUrl}`; + } + + // Remove ads & junk + $content + .find( + 'script, style, ins, iframe, [class*="abg"], [class*="ads"], [id*="ads"], [class*="google"], [id*="google"], [class*="recommend"], div[align="center"], p:contains("推薦本書"), a[href*="javascript:"]', + ) + .remove(); + + // Drop empty

+ $content.find('p').each((_i, el) => { + const $p = $(el); + if (!$p.text().trim()) $p.remove(); + }); + + // Remove HTML comments + $content + .contents() + .filter(function () { + return this.type === 'comment'; + }) + .remove(); + + // Unwrap + $content.find('font').each((_i, el) => { + const $el = $(el); + $el.replaceWith($el.html() || ''); + }); + + // Extract cleaned text + let chapterText = $content.html(); + if (!chapterText) return 'Error: Chapter content was empty'; + + chapterText = chapterText + .replace(/<\s*p[^>]*>/gi, '\n\n') + .replace(/<\s*br[^>]*>/gi, '\n'); + + chapterText = parseHTML(`

${chapterText}
`).text(); + + return chapterText + .replace(/[\t ]+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim(); + } + + async searchNovels( + searchTerm: string, + // pageNo: number, + ): Promise { + const searchUrl = `${this.site}bsearch?q=${encodeURIComponent(searchTerm)}`; + let body = ''; + + try { + const result = await fetchApi(searchUrl); + if (!result.ok) { + throw new Error( + `Failed to fetch search results: HTTP ${result.status}`, + ); + } + body = await result.text(); + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to fetch search results: ${error.message}`); + } + throw error; + } + + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('ul.u-list li.burl').each((_i, el) => { + const $el = $(el); + + const novelPath = $el.attr('data-url')?.trim(); + const novelName = $el.find('h3.bname a').text().trim(); + const novelCover = $el.find('.l-img img').attr('src')?.trim(); + + if (novelPath && novelName) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + } + }); + + return novels; + } +} + +export default new ixdzs8Plugin(); + +type ChapterJSON = { + rs: number; + data?: { + ctype: string; + ordernum: string; + title: string; + }[]; +}; diff --git a/plugins/chinese/linovel.ts b/plugins/chinese/linovel.ts new file mode 100644 index 000000000..b6ed952bc --- /dev/null +++ b/plugins/chinese/linovel.ts @@ -0,0 +1,165 @@ +import { fetchText } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; + +class LinovelPlugin implements Plugin.PluginBase { + id = 'linovel'; + name = 'linovel'; + icon = 'src/cn/linovel/icon.png'; + site = 'https://www.linovel.net'; + version = '1.0.1'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + private userAgent = + 'Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0'; + + private async fetchHTML(url: string) { + const body = await fetchText(url, { + headers: { 'User-Agent': this.userAgent }, + }); + if (body === '') throw Error('无法获取内容,请检查网络'); + return parseHTML(body); + } + + async popularNovels() // pageNo: number, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions, + : Promise { + const novels: Plugin.NovelItem[] = []; + + const loadedCheerio = await this.fetchHTML(this.site); + + loadedCheerio('a.book-item-inner').each((i, elem) => { + const name = loadedCheerio(elem).find('.book-item-name').text().trim(); + const path = loadedCheerio(elem).attr('href') ?? ''; + const cover = ( + loadedCheerio(elem).find('img').attr('data-original') ?? '' + ).replace(/!min300jpg$/, ''); + + novels.push({ name, path, cover }); + }); + return novels; + } + async parseNovel(novelPath: string): Promise { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + const loadedCheerio = await this.fetchHTML(this.site + novelPath); + + loadedCheerio('div.name') + .find('a') + .each((_i, elem) => { + novel.author = loadedCheerio(elem).text().trim(); + }); + + loadedCheerio('.book-title').each((_i, elem) => { + novel.name = loadedCheerio(elem).text().trim(); + }); + + loadedCheerio('.book-data') + .find('span') + .each((_i, elem) => { + const text = loadedCheerio(elem).text().trim(); + if (text.includes('字数')) { + // Handle word count + } else if (text.includes('热度')) { + // Handle popularity + } else if (text.includes('收藏')) { + // Handle favorites + } else if (text === '连载中') { + novel.status = NovelStatus.Ongoing; + } else if (text === '已完结') { + novel.status = NovelStatus.Completed; + } + }); + + loadedCheerio('.book-cover') + .find('img') + .each((_i, elem) => { + novel.cover = loadedCheerio(elem).attr('src') ?? ''; + }); + + novel.genres = loadedCheerio('.book-cats') + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.summary = loadedCheerio('.about-text').text().trim(); + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('div.chapter') + .find('a') + .each((i, elem) => { + const name = loadedCheerio(elem).text().trim(); + const path = loadedCheerio(elem).attr('href') ?? ''; + chapters.push({ name, path }); + }); + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise { + const loadedCheerio = await this.fetchHTML(this.site + chapterPath); + let chapterText = ''; + + if (loadedCheerio('.fufei-app-download-hint').length) { + throw Error('本章节需订阅后才能阅览,请下载轻之文库App订阅'); + } + + loadedCheerio('.article-text') + .find('p, img') + .each((i, elem) => { + if (elem.tagName === 'img') { + const imgSrc = loadedCheerio(elem).attr('src'); + chapterText += `\n`; + } else { + const text = loadedCheerio(elem).text().trim(); + if (text === ' ') { + chapterText += '
\n'; + } else { + chapterText += `

${text}

\n`; + } + } + }); + + return chapterText.trim(); + } + async searchNovels( + searchTerm: string, + // pageNo: number, + ): Promise { + const loadedCheerio = await this.fetchHTML( + this.site + '/search/?kw=' + searchTerm, + ); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.rank-book-list') + .find('a.search-book') + .each((i, elem) => { + const name = loadedCheerio(elem).find('.book-name').text().trim(); + const path = loadedCheerio(elem).attr('href') ?? ''; + const cover = ( + loadedCheerio(elem).find('img').attr('src') ?? '' + ).replace(/!min300jpg$/, ''); + + novels.push({ name, path, cover }); + }); + + return novels; + } +} + +export default new LinovelPlugin(); diff --git a/plugins/chinese/linovelib.ts b/plugins/chinese/linovelib.ts new file mode 100644 index 000000000..898d79a63 --- /dev/null +++ b/plugins/chinese/linovelib.ts @@ -0,0 +1,139 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchText } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { storage } from '@libs/storage'; + +class Linovelib implements Plugin.PluginBase { + id = 'linovelib'; + name = 'Linovelib'; + icon = 'src/cn/linovelib/icon.png'; + site = 'https://www.bilinovel.com'; + version = '1.2.1'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = { + method: 'GET', + headers: { + 'User-Agent': + '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', + }, + }; + webStorageUtilized = true; + // The URL of the custom LDS (Linovelib Descramble Server) URL. Due to complex de-scrambling logic, an external LDS is required. + pluginSettings = { + host: { + value: 'http://example.com', + label: 'Custom LDS Host', + type: 'Text', + }, + }; + serverUrl: string = storage.get('host') || 'http://localhost:5301'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const rank = showLatestNovels ? 'lastupdate' : filters.rank.value; + const url = `${this.site}/top/${rank}/${pageNo}.html`; + + const body = await fetchText(url); + if (body === '') throw Error('无法获取小说列表,请检查网络'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.module-rank-booklist .book-layout').each((i, el) => { + const url = loadedCheerio(el).attr('href'); + + const novelName = loadedCheerio(el).find('.book-title').text(); + const novelCover = loadedCheerio(el) + .find('div.book-cover > img') + .attr('data-src') + ?.replace('/https', 'https'); + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + path: url, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + // move major logic to LDS + const res = await fetchText( + `${this.serverUrl}/api/novel?path=${novelPath}`, + ); + const novel = JSON.parse(res) as Plugin.SourceNovel; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + // move major logic to LDS + const lastFetchChapterTime = + Number(storage.get('lastFetchChapterTime_' + chapterPath)) || 0; + if (Date.now() - lastFetchChapterTime < 10000) { + return storage.get('chapterContent_' + 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); + return resObj.content; + } + + async searchNovels( + searchTerm: string, + // pageNo: number, + ): Promise { + // move major logic to LDS + const lastSearchTime = + Number(storage.get('lastSearchTime_' + this.id)) || 0; + if (Date.now() - lastSearchTime < 5000) { + return []; + } + const res = await fetchText( + `${this.serverUrl}/api/search?keyword=${encodeURIComponent(searchTerm)}`, + ); + const novelsData = JSON.parse(res).results as Plugin.NovelItem[]; + storage.set('lastSearchTime_' + this.id, Date.now()); + return novelsData; + } + + filters = { + rank: { + label: '排行榜', + value: 'monthvisit', + options: [ + { label: '月点击榜', value: 'monthvisit' }, + { label: '周点击榜', value: 'weekvisit' }, + { label: '月推荐榜', value: 'monthvote' }, + { label: '周推荐榜', value: 'weekvote' }, + { label: '月鲜花榜', value: 'monthflower' }, + { label: '周鲜花榜', value: 'weekflower' }, + { label: '月鸡蛋榜', value: 'monthegg' }, + { label: '周鸡蛋榜', value: 'weekegg' }, + { label: '最近更新', value: 'lastupdate' }, + { label: '最新入库', value: 'postdate' }, + { label: '收藏榜', value: 'goodnum' }, + { label: '新书榜', value: 'newhot' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Linovelib(); diff --git a/plugins/chinese/linovelib_tw.ts b/plugins/chinese/linovelib_tw.ts new file mode 100644 index 000000000..ffa8d834a --- /dev/null +++ b/plugins/chinese/linovelib_tw.ts @@ -0,0 +1,546 @@ +import { CheerioAPI, 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'; + +class Linovelib_tw implements Plugin.PluginBase { + id = 'linovelib_tw'; + name = 'Linovelib(繁體)'; + icon = 'src/cn/linovelib/icon.png'; + site = 'https://tw.linovelib.com/'; + version = '1.0.1'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const rank = showLatestNovels ? 'lastupdate' : filters.rank.value; + const url = `${this.site}/top/${rank}/${pageNo}.html`; + + const body = await fetchText(url); + if (body === '') throw Error('無法獲取小説列表, 請檢查網絡'); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.module-rank-booklist .book-layout').each((i, el) => { + const url = loadedCheerio(el).attr('href'); + + const novelName = loadedCheerio(el).find('.book-title').text(); + const novelCover = loadedCheerio(el) + .find('div.book-cover > img') + .attr('data-src'); + if (!url) return; + + const novel = { + name: novelName, + cover: novelCover, + path: url, + }; + + novels.push(novel); + }); + + return novels; + } + + 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', + ); + + novel.summary = loadedCheerio('#bookSummary content').text(); + + novel.author = loadedCheerio('#bookDetailWrapper .book-rand-a 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; + + 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 chapterName = + volumeName + + ' — ' + + chaptersLoadedCheerio(el).find('.chapter-index').text().trim(); + const releaseDate = null; + + if (!chapterId) return; + + chapter.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl, + }); + }, + ); + + 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 () => { + // Remove JS and notice of the website + pageCheerio('div.cgo, center').remove(); + + // Load lazyloaded images + pageCheerio('[id*=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('[id*=acontent]').html() || ''; + pageText = pageText.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; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + const url = `${this.site}/search/${encodeURI(searchTerm)}_${pageNo}.html`; + + 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, + // }); + // } + // }; + + // NOTE: don't know redirect is for what, comment out for now + + // const redirect = pageCheerio("div.book-layout").text(); + // await addPage(pageCheerio, redirect); + + pageCheerio('.book-ol .book-layout').each((i, el) => { + const 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'); + + if (!nUrl) return; + + novels.push({ + name: novelName, + path: nUrl, + cover: novelCover, + }); + }); + + return novels; + } + + filters = { + rank: { + label: '排行榜', + value: 'monthvisit', + options: [ + { label: '月點擊榜', value: 'monthvisit' }, + { label: '周點擊榜', value: 'weekvisit' }, + { label: '月推薦榜', value: 'monthvote' }, + { label: '周推薦榜', value: 'weekvote' }, + { label: '月鮮花榜', value: 'monthflower' }, + { label: '周鮮花榜', value: 'weekflower' }, + { label: '月鷄蛋榜', value: 'monthegg' }, + { label: '周鷄蛋榜', value: 'weekegg' }, + { label: '最近更新', value: 'lastupdate' }, + { label: '最新入庫', value: 'postdate' }, + { label: '收藏榜', value: 'goodnum' }, + { label: '新書榜', value: 'newhot' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Linovelib_tw(); diff --git a/plugins/chinese/novel543.ts b/plugins/chinese/novel543.ts new file mode 100644 index 000000000..054467256 --- /dev/null +++ b/plugins/chinese/novel543.ts @@ -0,0 +1,328 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +const makeAbsolute = ( + relativeUrl: string | undefined, + baseUrl: string, +): string | undefined => { + if (!relativeUrl) return undefined; + try { + if (relativeUrl.startsWith('//')) { + return new URL(baseUrl).protocol + relativeUrl; + } + if ( + relativeUrl.startsWith('http://') || + relativeUrl.startsWith('https://') + ) { + return relativeUrl; + } + return new URL(relativeUrl, baseUrl).href; + } catch { + return undefined; + } +}; + +class Novel543Plugin implements Plugin.PluginBase { + id = 'novel543'; + name = 'Novel543'; + site = 'https://www.novel543.com/'; + version = '1.0.0'; + icon = 'src/cn/novel543/icon.png'; + + imageRequestInit = { + headers: { + Referer: this.site, + }, + }; + + async popularNovels(pageNo: number): Promise { + if (pageNo > 1) return []; + + const result = await fetchApi(this.site); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const novels: Plugin.NovelItem[] = []; + const processedPaths = new Set(); + + $('ul.list > li.media, ul.list li > a[href^="/"][href$="/"]').each( + (_i, el) => { + const $el = $(el); + let novelPath: string | undefined; + let novelName: string | undefined; + let novelCover: string | undefined; + + if ($el.is('li.media')) { + const $link = $el.find('.media-content h3 a'); + novelPath = $link.attr('href')?.trim(); + novelName = $link.text().trim(); + novelCover = $el.find('.media-left img').attr('src')?.trim(); + } else if ($el.is('a')) { + novelPath = $el.attr('href')?.trim(); + novelName = + $el.find('h3, b, span').first().text().trim() || + $el.parent().find('h3').text().trim() || + $el.text().trim(); + novelCover = + $el.find('img').attr('src')?.trim() || + $el.parent().find('img').attr('src')?.trim(); + } + + if ( + novelPath && + novelName && + novelPath.match(/^\/\d+\/$/) && + !processedPaths.has(novelPath) + ) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + processedPaths.add(novelPath); + } + }, + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const novelUrl = makeAbsolute(novelPath, this.site); + if (!novelUrl) throw new Error('Invalid novel URL'); + + const result = await fetchApi(novelUrl); + if (!result.ok) throw new Error('Failed to fetch novel'); + + const $ = parseHTML(await result.text()); + const $infoSection = $('section#detail div.media-content.info'); + const $modSection = $('section#detail div.mod'); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $infoSection.find('h1.title').text().trim() || 'Untitled', + cover: + makeAbsolute( + $('section#detail div.cover img').attr('src'), + this.site, + ) || defaultCover, + summary: $modSection.find('div.intro').text().trim() || undefined, + author: + $infoSection.find('p.meta span.author').text().trim() || undefined, + genres: + $infoSection + .find('p.meta a[href*="/bookstack/"]') + .map((_i, el) => $(el).text().trim()) + .get() + .join(', ') || undefined, + status: NovelStatus.Unknown, + chapters: [], + }; + + const chapterListPath = + $modSection + .find('p.action.buttons a.button.is-info[href$="/dir"]') + .attr('href') || + $infoSection.find('a.button.is-info[href$="/dir"]').attr('href'); + + if (chapterListPath) { + novel.chapters = await this.parseChapterList(chapterListPath); + } + + return novel; + } + + async parseChapterList( + chapterListPath: string, + ): Promise { + const chapterListUrl = makeAbsolute(chapterListPath, this.site); + if (!chapterListUrl) return []; + + const result = await fetchApi(chapterListUrl); + if (!result.ok) return []; + + const $ = parseHTML(await result.text()); + const chapters: Plugin.ChapterItem[] = []; + + $('div.chaplist ul.all li a').each((index, el) => { + const $el = $(el); + const chapterName = $el.text().trim(); + const chapterUrl = $el.attr('href')?.trim(); + + if (chapterName && chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl, + chapterNumber: index + 1, + }); + } + }); + + const sortButtonText = $('div.chaplist .header button.reverse span') + .last() + .text() + .trim(); + if (sortButtonText === '倒序') { + chapters.reverse(); + chapters.forEach((chap, index) => (chap.chapterNumber = index + 1)); + } + + return chapters; + } + + async parseChapter(chapterPath: string): Promise { + const chapterUrl = makeAbsolute(chapterPath, this.site); + if (!chapterUrl) throw new Error('Invalid chapter URL'); + + const result = await fetchApi(chapterUrl); + if (!result.ok) throw new Error('Failed to fetch chapter'); + + const $ = parseHTML(await result.text()); + const $content = $('div.content.py-5'); + if (!$content.length) return 'Error: Could not find chapter content'; + + $content + .find( + 'script, style, ins, iframe, [class*="ads"], [id*="ads"], [class*="google"], [id*="google"], [class*="recommend"], div[align="center"], p:contains("推薦本書"), a[href*="javascript:"]', + ) + .remove(); + + $content.find('p').each((_i, el) => { + const $p = $(el); + const pText = $p.text().trim(); + if ( + pText.includes('請記住本站域名') || + pText.includes('手機版閱讀網址') || + pText.includes('novel543') || + pText.includes('稷下書院') || + pText.includes('最快更新') || + pText.includes('最新章節') || + pText.includes('章節報錯') || + pText.match(/app|APP|下載|客户端|关注微信|公众号/i) || + pText.length === 0 || + ($p + .html() + ?.replace(/ /g, '') + .trim() === '' && + $p.find('img').length === 0) || + pText.includes('溫馨提示') + ) { + $p.remove(); + } + }); + + $content + .contents() + .filter(function () { + return this.type === 'comment'; + }) + .remove(); + + let chapterText = $content.html(); + if (!chapterText) return 'Error: Chapter content was empty'; + + chapterText = chapterText + .replace(/<\s*p[^>]*>/gi, '\n\n') + .replace(/<\s*br[^>]*>/gi, '\n'); + + chapterText = parseHTML(`
${chapterText}
`).text(); + + return chapterText + .replace(/[\t ]+/g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim(); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo > 1) return []; + + if (/^\d+$/.test(searchTerm)) { + try { + const novelPath = `/${searchTerm}/`; + const novel = await this.parseNovel(novelPath); + return [ + { + name: novel.name, + path: novelPath, + cover: novel.cover, + }, + ]; + } catch { + return []; + } + } + + const searchUrl = `${this.site}search/${encodeURIComponent(searchTerm)}`; + let body = ''; + + try { + const result = await fetchApi(searchUrl); + if (!result.ok) { + if (result.status === 403 || result.status === 503) { + throw new Error( + 'Cloudflare protection detected (HTTP error). Please try opening the plugin in WebView first to solve the challenge.', + ); + } + return []; + } + + body = await result.text(); + const $ = parseHTML(body); + const pageTitle = $('title').text().toLowerCase(); + + // Check for various Cloudflare challenge indicators + if ( + pageTitle.includes('attention required') || + pageTitle.includes('just a moment') || + pageTitle.includes('please wait') || + pageTitle.includes('verifying') || + body.includes('Verifying you are human') || + body.includes('cf-browser-verification') || + body.includes('cf_captcha_container') + ) { + throw new Error( + 'Cloudflare protection detected. Please try opening the plugin in WebView first to solve the challenge.', + ); + } + } catch (error) { + if (error instanceof Error) { + // If it's already our custom error, re-throw it + if (error.message.includes('Cloudflare protection detected')) { + throw error; + } + // For other errors, throw a generic error + throw new Error(`Failed to fetch search results: ${error.message}`); + } + throw error; + } + + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('div.search-list ul.list > li.media').each((_i, el) => { + const $el = $(el); + const $link = $el.find('.media-content h3 a'); + const novelPath = $link.attr('href')?.trim(); + const novelName = $link.text().trim(); + const novelCover = $el.find('.media-left img').attr('src')?.trim(); + + if (novelPath && novelName && novelPath.match(/^\/\d+\/$/)) { + novels.push({ + name: novelName, + path: novelPath, + cover: makeAbsolute(novelCover, this.site) || defaultCover, + }); + } + }); + + return novels; + } +} + +export default new Novel543Plugin(); diff --git a/plugins/english/NovelOnline.ts b/plugins/english/NovelOnline.ts new file mode 100644 index 000000000..d54a5f179 --- /dev/null +++ b/plugins/english/NovelOnline.ts @@ -0,0 +1,254 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi, FetchInit } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class NovelsOnline implements Plugin.PluginBase { + id = 'NO.net'; + name = 'novelsOnline'; + site = 'https://novelsonline.org'; + icon = 'src/en/novelsonline/icon.png'; + version = '1.0.2'; + + async safeFetch( + url: string, + init: FetchInit | undefined = undefined, + ): Promise { + const r = await fetchApi(url, init); + if (!r.ok) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const body = await r.text(); + const $ = parseHTML(body); + + // Check if the input is random characters + // the title element should be empty only if the input is random characters + const hasElementNodes = $('title') !== undefined; + if (!hasElementNodes) + throw new Error( + 'Captcha protection detected (Input is random characters). Please try opening the page in WebView.', + ); + + return $; + } + + async parseNovels(loadedCheerio: CheerioAPI): Promise { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.top-novel-block').each((i, el) => { + const novelName = loadedCheerio(el).find('h2').text(); + const novelCover = loadedCheerio(el) + .find('.top-novel-cover img') + .attr('src'); + const novelUrl = loadedCheerio(el).find('h2 a').attr('href'); + if (!novelUrl) return; + + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }); + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + const form = new URLSearchParams(); + + for (const key in filters) { + if (key === 'keyword') { + form.append('keyword', filters[key].value as string); + } else if (typeof filters[key].value === 'object') { + for (const value of filters[key].value as string[]) + form.append(`include[${key}][]`, value); + } else if (filters[key].value) { + form.append(`include[${key}][]`, filters[key].value as string); + } + } + if (form.toString()) { + form.append('search', '1'); + return page == 1 ? this.detailedSearch(form) : []; + } + + const $ = await this.safeFetch(this.site + '/top-novel/' + page); + return this.parseNovels($); + } + + async searchNovels(searchTerm: string): Promise { + const form = new URLSearchParams(); + form.append('keyword', searchTerm); + form.append('search', '1'); + + return this.detailedSearch(form); + } + + async detailedSearch(form: URLSearchParams): Promise { + const $ = await this.safeFetch(this.site + '/detailed-search', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: form.toString(), + }); + + return this.parseNovels($); + } + + async parseNovel(novelPath: string): Promise { + const $ = await this.safeFetch(this.site + novelPath); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: $('h1').text() || 'Untitled', + cover: $('.novel-cover').find('a > img').attr('src'), + chapters: [], + }; + + $('.novel-detail-item').each((i, el) => { + const detailName = $(el).find('h6').text(); + const detail = $(el).find('.novel-detail-body'); + + switch (detailName) { + case 'Description': + novel.summary = detail.text(); + break; + case 'Genre': + novel.genres = detail + .find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + break; + case 'Author(s)': + novel.author = detail + .find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + break; + case 'Artist(s)': + { + const artist = detail + .find('li') + .map((_, el) => $(el).text()) + .get() + .join(', '); + if (artist && artist != 'N/A') novel.artist = artist; + } + break; + case 'Status': + novel.status = detail.text().trim(); + break; + } + }); + + novel.chapters = $('ul.chapter-chs > li > a') + .map((_, el) => { + const chapterUrl = $(el).attr('href'); + const chapterName = $(el).text(); + + return { + name: chapterName, + path: chapterUrl?.replace(this.site, ''), + } as Plugin.ChapterItem; + }) + .get(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const loadedCheerio = await this.safeFetch(this.site + chapterPath); + + const chapterText = loadedCheerio('#contentall').html() || ''; + + return chapterText; + } + + filters = { + keyword: { + value: '', + label: 'Keyword', + type: FilterTypes.TextInput, + }, + novel_type: { + value: [], + label: 'Novel Type', + options: [ + { label: 'Web Novel', value: 'Web Novel' }, + { label: 'Light Novel', value: 'Light Novel' }, + { label: 'Chinese Novel', value: 'Chinese Novel' }, + { label: 'Korean Novel', value: 'Korean Novel' }, + ], + type: FilterTypes.CheckboxGroup, + }, + language: { + value: [], + label: 'Language', + options: [ + { label: 'Chinese', value: 'Chinese' }, + { label: 'Japanese', value: 'Japanese' }, + { label: 'Korean', value: 'Korean' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genre', + options: [ + { label: 'Action', value: '4' }, + { label: 'Adventure', value: '1' }, + { label: 'Celebrity', value: '39' }, + { label: 'Comedy', value: '12' }, + { label: 'Drama', value: '6' }, + { label: 'Ecchi', value: '47' }, + { label: 'Fantasy', value: '2' }, + { label: 'Gender Bender', value: '14' }, + { label: 'Harem', value: '45' }, + { label: 'Historical', value: '22' }, + { label: 'Horror', value: '31' }, + { label: 'Josei', value: '21' }, + { label: 'Martial Arts', value: '18' }, + { label: 'Mature', value: '46' }, + { label: 'Mecha', value: '30' }, + { label: 'Mystery', value: '7' }, + { label: 'Psychological', value: '8' }, + { label: 'Romance', value: '9' }, + { label: 'School Life', value: '10' }, + { label: 'Sci-fi', value: '3' }, + { label: 'Seinen', value: '23' }, + { label: 'Shotacon', value: '35' }, + { label: 'Shoujo', value: '11' }, + { label: 'Shoujo Ai', value: '34' }, + { label: 'Shounen', value: '5' }, + { label: 'Shounen Ai', value: '32' }, + { label: 'Slice of Life', value: '13' }, + { label: 'Sports', value: '33' }, + { label: 'Supernatural', value: '25' }, + { label: 'Tragedy', value: '24' }, + { label: 'Wuxia', value: '17' }, + { label: 'Xianxia', value: '20' }, + { label: 'Xuanhuan', value: '38' }, + { label: 'Yaoi', value: '16' }, + { label: 'Yuri', value: '27' }, + ], + type: FilterTypes.CheckboxGroup, + }, + completed: { + value: '', + label: 'Completed', + options: [ + { label: 'Any', value: '' }, + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new NovelsOnline(); diff --git a/plugins/english/StorySeedling.ts b/plugins/english/StorySeedling.ts new file mode 100644 index 000000000..9c0171d2f --- /dev/null +++ b/plugins/english/StorySeedling.ts @@ -0,0 +1,288 @@ +import { CheerioAPI, load } from 'cheerio'; +import { Plugin } from '@/types/plugin'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs, { ManipulateType } from 'dayjs'; + +type NovelJSON = { + success: boolean; + data: { + posts: { + title: string; + thumbnail: string; + permalink: string; + }[]; + }; +}; + +type ChapterJSON = { + success: boolean; + data: { + title: string; + url: string; + slug: string; + date: string; + }[]; +}; + +class StorySeedlingPlugin implements Plugin.PluginBase { + id = 'storyseedling'; + name = 'StorySeedling'; + icon = 'src/en/storyseedling/icon.png'; + site = 'https://storyseedling.com/'; + version = '1.0.6'; + nonce: string | undefined; + + async getCheerio(url: string, search: boolean): Promise { + const r = await fetchApi(url); + if (!r.ok && search != true) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const $ = load(await r.text()); + + return $; + } + + private parseAgoDate(date: string | undefined) { + //parseMadaraDate + const parsed = dayjs(date); + if (date && parsed.isValid()) { + return parsed.toISOString(); + } + + const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || []; + const decade = time?.includes('decade'); // dayjs no support, but just in case + const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1); + const unit = (decade ? 'year' : time) as ManipulateType; + + const validUnits = [ + 'millisecond', // waow + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'year', + ]; + + if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) { + return null; + } + + return dayjs().subtract(amount, unit).toISOString(); + } + + private async getNovels( + pageNo: number, + searchTerm = '', + ): Promise { + const body = await fetchApi(this.site + 'browse').then(r => r.text()); + const loadedCheerio = load(body); + + const postValue = loadedCheerio('div[ax-load][x-data]') + .attr('x-data') + ?.replace("browse('", '') + .replace("')", '') as string; + + const data = new FormData(); + data.append('search', searchTerm); + data.append('orderBy', 'recent'); + data.append('curpage', pageNo.toString()); + data.append('post', postValue); + data.append('action', 'fetch_browse'); + + const response = (await fetchApi(this.site + 'ajax', { + body: data, + method: 'POST', + }).then(res => res.json())) as NovelJSON; + + const novels: Plugin.NovelItem[] = response.data.posts.map(element => ({ + name: element.title, + cover: element.thumbnail, + path: element.permalink.replace(this.site, ''), + })); + + return novels; + } + + async popularNovels(pageNo: number): Promise { + return this.getNovels(pageNo); + } + + async parseNovel(novelPath: string): Promise { + const $ = await this.getCheerio(this.site + novelPath, false); + + const novel: Partial = { + path: novelPath, + }; + + novel.name = $('h1').text().trim(); + const coverUrl = $('img[x-ref="art"].w-full.rounded.shadow-md').attr('src'); + + if (coverUrl) { + novel.cover = new URL(coverUrl, this.site).href; + } + + const genres: string[] = []; + $( + 'section[x-data="{ tab: location.hash.substr(1) || \'chapters\' }"].relative > div > div > div.flex.flex-wrap > a', + ).each(function () { + genres.push($(this).text().trim()); + }); + novel.genres = genres.join(', '); + + novel.author = $('div.mb-1 a').text().trim(); + + const rawStatus = $('div.gap-2 span.text-sm').text().trim(); + const map: Record = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + }; + novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown; + + const summaryDiv = $('div.mb-4.order-2:not(.lg\\:grid-in-buttons)'); + const pTagSummary = summaryDiv.find('p'); + novel.summary = + pTagSummary.length > 0 + ? pTagSummary + .map((_, el) => $(el).text().trim()) + .get() + .join('\n\n') + : summaryDiv.text().trim(); // --- Else (no

tags) --- + + const chapters: Plugin.ChapterItem[] = []; + + const xdata = $('.bg-accent div[ax-load][x-data]').attr('x-data'); + // expected data format: toc('000000', 'xxxxxxxxxx') (6 numbers, 10 hex) + if (xdata) { + const listXdata = xdata?.split("'"); + const dataNovelId = listXdata[1]; + const dataNovelN = listXdata[3]; + const idMatch = novelPath.match(/\d+/); // Error checking in case of extraneous slashes + const novelId = dataNovelId || (idMatch ? idMatch[0] : null); + + if (novelId) { + const chapterListing = 'ajax'; // Currently URL is only ajax + + const formData = new FormData(); + formData.append('post', dataNovelN); + formData.append('id', dataNovelId); + formData.append('action', 'series_toc'); + + const chaptersUrl = `${this.site}${chapterListing}`; + const refererUrl = `${this.site}${novel.path}`; + + const results: ChapterJSON = await fetchApi(chaptersUrl, { + method: 'POST', + referrer: refererUrl, + referrerPolicy: 'origin', + body: formData, + }) + .then(r => r.json()) + .catch( + e => + (novel.summary = + 'Chapter Parse Error: ' + e + '\n\n' + novel.summary), + ); + + if (results.data) { + results.data.forEach(chap => { + if (chap.url == null) { + return; + } + const name = chap.title; + const url = chap.url as string; + const chapterNumber = chap.slug; + + chapters.push({ + name: name, + path: url.replace(this.site, ''), + releaseTime: this.parseAgoDate(chap.date), + chapterNumber: parseInt(chapterNumber), + }); + }); + } + } + } + + novel.chapters = chapters; + + return novel as Plugin.SourceNovel; + } + + async updateNonce(chapterPath: string) { + const $ = await this.getCheerio(this.site + chapterPath, false); + this.nonce = $('div.mb-4:has(h1.text-xl) > div') + .attr('x-data') + ?.match(/loadChapter\('.+?', '(.+?)'\)/)![1]; + } + + async parseChapter(chapterPath: string): Promise { + const updatedNonce = !this.nonce; + if (!this.nonce) await this.updateNonce(chapterPath); + const text = await fetchApi(this.site + chapterPath + '/content', { + method: 'POST', + headers: { + 'referrer': this.site + chapterPath + '/', + 'x-nonce': this.nonce, + }, + body: JSON.stringify({ 'captcha_response': '' }), + }).then(r => r.text()); + let textJson; + try { + textJson = JSON.parse(text); + } catch (_) { + //not json :fire: we have chapter + } + if (textJson && !textJson.success) { + if (textJson.message === 'Invalid security.') { + if (updatedNonce) { + throw new Error(`Failed to find code!`); + } + this.nonce = ''; + return await this.parseChapter(chapterPath); + } + if (textJson.captcha) { + throw new Error( + `Failed to bypass turnstile captcha (read in webview until it stops ig)`, + ); + } + } + const html = text + .replace(/cls[a-f0-9]+/g, '') + .split('') + .map(char => { + const code = char.charCodeAt(0); + const offset = code > 12123 ? 12027 : 12033; + const decoded = code - offset; + return decoded >= 32 && decoded <= 126 + ? String.fromCharCode(decoded) + : char; + }) + .join(''); + const $ = load(html); + + $('span').text((_, txt) => + txt.toLowerCase().includes('storyseedling') || + txt.toLowerCase().includes('story seedling') + ? '' + : txt, + ); + + return $.html(); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + return this.getNovels(pageNo, searchTerm); + } +} + +export default new StorySeedlingPlugin(); diff --git a/plugins/english/ao3.ts b/plugins/english/ao3.ts new file mode 100644 index 000000000..8979df382 --- /dev/null +++ b/plugins/english/ao3.ts @@ -0,0 +1,600 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class ArchiveOfOurOwn implements Plugin.PluginBase { + id = 'archiveofourown'; + name = 'Archive Of Our Own'; + version = '1.0.4'; + icon = 'src/en/ao3/icon.png'; + site = 'https://archiveofourown.org/'; + + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('li.work').each((idx, ele) => { + const novelName = loadedCheerio(ele) + .find('h4.heading > a') + .first() + .text() + .trim(); + const novelUrl = loadedCheerio(ele) + .find('h4.heading > a') + .first() + .attr('href') + ?.trim(); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: defaultCover, // No cover image + path: novelUrl.slice(1), + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const params = new URLSearchParams({ + commit: 'Search', + page: page.toString(), + 'work_search[language_id]': filters.language.value, + }); + + if (showLatestNovels) { + params.set('work_search[sort_column]', 'revised_at'); + } else { + params.set('work_search[sort_column]', filters.sort.value); + } + params.set('work_search[sort_direction]', filters.sortdir.value); + + // we could send in the entire thing without checking for blanks + if (filters.completion.value !== '') { + params.set('work_search[complete]', filters.completion.value); + } + if (filters.crossover.value !== '') { + params.set('work_search[crossover]', filters.crossover.value); + } + if (filters.categories.value.length > 0) { + filters.categories.value.forEach((category: string) => { + params.append('work_search[category_ids][]', category); + }); + } + if (filters.warningsFilter.value.length > 0) { + filters.warningsFilter.value.forEach((warning: string) => { + params.append('work_search[archive_warning_ids][]', warning); + }); + } + if (filters.singlechap.value) { + params.set('work_search[single_chapter]', '1'); + } + if (filters.author.value !== '') { + params.set('work_search[creators]', filters.author.value); + } + if ( + filters.dateFilter.value !== '' && + filters.dateIncrements.value !== '' + ) { + params.set( + 'work_search[revised_at]', + `${filters.dateFilter.value} ${filters.dateIncrements.value}`, + ); + } + if (filters.words.value !== '') { + params.set('work_search[word_count]', filters.words.value); + } + if (filters.hits.value !== '') { + params.set('work_search[hits]', filters.hits.value); + } + if (filters.bookmarks.value !== '') { + params.set('work_search[bookmarks_count]', filters.bookmarks.value); + } + if (filters.comments.value !== '') { + params.set('work_search[comments_count]', filters.comments.value); + } + if (filters.kudos.value !== '') { + params.set('work_search[kudos_count]', filters.kudos.value); + } + + const link = `${this.site}works/search?${params.toString()}`; + const body = await fetchApi(link).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelUrl: string): Promise { + const result = await fetchApi(new URL(novelUrl, this.site).toString()); + const urlchapter = novelUrl + '/navigate'; + const chapters = await fetchApi(new URL(urlchapter, this.site).toString()); + const body = await result.text(); + const chapterlisttext = await chapters.text(); + const chapterlistload = parseHTML(chapterlisttext); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelUrl, + name: loadedCheerio('h2.title').text().trim() || 'Untitled', + cover: defaultCover, // No cover image available + status: loadedCheerio('dt.status').text().includes('Updated') + ? 'Ongoing' + : 'Completed', + chapters: [], + }; + + novel.author = loadedCheerio('a[rel="author"]') + .map((i, el) => loadedCheerio(el).text().trim()) + .get() + .join(', '); + novel.genres = Array.from(loadedCheerio('dd.freeform.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const summary = loadedCheerio('blockquote.userstuff').text().trim(); + const fandom = Array.from(loadedCheerio('dd.fandom.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const rating = Array.from(loadedCheerio('dd.rating.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const warning = Array.from(loadedCheerio('dd.warning.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const series = Array.from(loadedCheerio('dd.series li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const relation = Array.from(loadedCheerio('dd.relationship.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const character = Array.from(loadedCheerio('dd.character.tags li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + const stats = Array.from(loadedCheerio('dd.stats li a.tag')) + .map(el => loadedCheerio(el).text().trim()) + .join(','); + novel.summary = `Fandom:\n${fandom}\n\nRating:\n${rating}\n\nWarning:\n${warning}\n\nSummary:\n${summary}\n\nSeries:\n${series}\n\nRelationships:\n${relation}\n\nCharacters:\n${character}\n\nStats:\n${stats}`; + const chapterItems: Plugin.ChapterItem[] = []; + const longReleaseDate: string[] = []; + // let match: RegExpExecArray | null; + chapterlistload('ol.index').each((i, ele) => { + chapterlistload(ele) + .find('li') + .each((i, el) => { + // const chapterNameMatch = chapterlistload(el).find('a').text().trim(); + const releaseTimeText = chapterlistload(el) + .find('span.datetime') + .text() + .replace(/\(([^)]+)\)/g, '$1') + .trim(); + const releaseTime = releaseTimeText + ? new Date(releaseTimeText).toISOString() + : ''; + longReleaseDate.push(releaseTime); + }); + }); + const releaseTimeText = loadedCheerio('.wrapper dd.published') + .text() + .trim(); + const releaseTime = releaseTimeText + ? new Date(releaseTimeText).toISOString() + : ''; + let dateCounter = 0; + if (loadedCheerio('#chapter_index select').length > 0) { + loadedCheerio('#chapter_index select').each((i, selectEl) => { + loadedCheerio(selectEl) + .find('option') + .each((i, el) => { + const chapterName = loadedCheerio(el).text().trim(); + const chapterUrlCode = loadedCheerio(el).attr('value')?.trim(); + const chapterUrl = `${novelUrl}/chapters/${chapterUrlCode}`; + const releaseDate: string = longReleaseDate[dateCounter]; + dateCounter++; + if (chapterUrl) { + chapterItems.push({ + name: chapterName, + path: chapterUrl, + releaseTime: releaseDate, + }); + } + }); + }); + } + if (chapterItems.length === 0) { + loadedCheerio('#chapters h3.title').each((i, titleEl) => { + const fullTitleText = loadedCheerio(titleEl).text().trim(); + const chapterNameMatch = fullTitleText.match(/:\s*(.*)$/); + let chapterName = chapterNameMatch ? chapterNameMatch[1].trim() : ''; + const chapterUrlRaw = loadedCheerio(titleEl) + .find('a') + .attr('href') + ?.trim(); + const chapterUrlCode = chapterUrlRaw?.match(/\/chapters\/(\d+)/)?.[1]; + const chapterUrl = `${novelUrl}/chapters/${chapterUrlCode}`; + + if (chapterUrl) { + if (chapterName === '') { + const novelTitle = loadedCheerio('.work .title.heading') + .text() + .trim(); + chapterName = novelTitle; + } + chapterItems.push({ + name: chapterName, + path: chapterUrl, + releaseTime: releaseTime, + }); + } + }); + if (chapterItems.length === 0) { + loadedCheerio('.work.navigation.actions li a').each((i, el) => { + const href = loadedCheerio(el).attr('href'); + if (href && href.includes('/downloads/')) { + const chapterUrlCodeMatch = href.match(/updated_at=(\d+)/); + const chapterUrlCode = chapterUrlCodeMatch + ? chapterUrlCodeMatch[1] + : null; + let chapterName = loadedCheerio('h2.title.heading').text().trim(); + + const chapterUrl = `${novelUrl}/chapters/${chapterUrlCode}`; + + if (chapterUrl) { + if (chapterName === '') { + const novelTitle = loadedCheerio('.work .title.heading') + .text() + .trim(); + chapterName = novelTitle; + } + chapterItems.push({ + name: chapterName, + path: chapterUrl, + releaseTime: releaseTime, + }); + } + } + }); + } + } + novel.chapters = chapterItems; + + return novel; + } + + async parseChapter(chapterUrl: string): Promise { + const result = await fetchApi(new URL(chapterUrl, this.site).toString()); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + loadedCheerio('h3.title').each((i, el) => { + const $h3 = loadedCheerio(el); + const $a = $h3.find('a'); + $a.removeAttr('href'); + const aText = $a.text().trim(); + const nextSiblingText = $h3 + .contents() + .filter((_, node) => node.nodeType === 3) + .text() + .trim(); + $h3.html(`${aText}
${nextSiblingText}`); + }); + loadedCheerio('h3.landmark.heading#work').remove(); + + const chapterText = loadedCheerio('div#chapters > div').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const params = new URLSearchParams({ + commit: 'Search', + page: page.toString(), + 'work_search[language_id]': 'en', + 'work_search[query]': searchTerm, + }); + const searchUrl = `${this.site}works/search?${params.toString()}`; + + const result = await fetchApi(searchUrl); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + filters = { + sort: { + value: 'hits', + label: 'Sort by', + options: [ + { label: 'Best Match', value: '_score' }, + { label: 'Hits', value: 'hits' }, + { label: 'Kudos', value: 'kudos' }, + { label: 'Comments', value: 'comments' }, + { label: 'Bookmarks', value: 'bookmarks' }, + { label: 'Word Count', value: 'word_count' }, + { label: 'Date Updated', value: 'revised_at' }, + { label: 'Date Posted', value: 'created_at' }, + { label: 'Author', value: 'authors_to_sort_on' }, + { label: 'Title', value: 'title_to_sort_on' }, + ], + type: FilterTypes.Picker, + }, + sortdir: { + value: 'desc', + label: 'Sort direction', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + ratings: { + value: '', + label: 'Ratings', + options: [ + { label: 'Not Rated', value: '9' }, + { label: 'General Audiences', value: '10' }, + { label: 'Teen And Up Audiences', value: '11' }, + { label: 'Mature', value: '12' }, + { label: 'Explicit', value: '13' }, + ], + type: FilterTypes.Picker, + }, + language: { + value: 'en', + label: 'Language', + options: [ + { label: 'None', value: '' }, + { label: 'af Soomaali', value: 'so' }, + { label: 'Afrikaans', value: 'afr' }, + { label: 'Aynu itak | アイヌ イタㇰ', value: 'ain' }, + { label: 'العربية', value: 'ar' }, + { label: 'አማርኛ', value: 'amh' }, + { label: '𓂋𓏺𓈖 𓆎𓅓𓏏𓊖', value: 'egy' }, + { label: 'ܐܪܡܝܐ | ארמיא', value: 'arc' }, + { label: 'հայերեն', value: 'hy' }, + { label: 'American Sign Language', value: 'ase' }, + { label: 'asturianu', value: 'ast' }, + { label: 'Bahasa Indonesia', value: 'id' }, + { label: 'Bahasa Malaysia', value: 'ms' }, + { label: 'Български', value: 'bg' }, + { label: 'বাংলা', value: 'bn' }, + { label: 'Basa Jawa', value: 'jv' }, + { label: 'Башҡорт теле', value: 'ba' }, + { label: 'беларуская', value: 'be' }, + { label: 'Bosanski', value: 'bos' }, + { label: 'Brezhoneg', value: 'br' }, + { label: 'Català', value: 'ca' }, + { label: 'Cebuano', value: 'ceb' }, + { label: 'Čeština', value: 'cs' }, + { label: 'Chinuk Wawa', value: 'chn' }, + { label: 'къырымтатар тили | qırımtatar tili', value: 'crh' }, + { label: 'Cymraeg', value: 'cy' }, + { label: 'Dansk', value: 'da' }, + { label: 'Deutsch', value: 'de' }, + { label: 'eesti keel', value: 'et' }, + { label: 'Ελληνικά', value: 'el' }, + { label: '𒅴𒂠', value: 'sux' }, + { label: 'English', value: 'en' }, + { label: 'Eald Englisċ', value: 'ang' }, + { label: 'Español', value: 'es' }, + { label: 'Esperanto', value: 'eo' }, + { label: 'Euskara', value: 'eu' }, + { label: 'فارسی', value: 'fa' }, + { label: 'Filipino', value: 'fil' }, + { label: 'Français', value: 'fr' }, + { label: 'Friisk', value: 'frr' }, + { label: 'Furlan', value: 'fur' }, + { label: 'Gaeilge', value: 'ga' }, + { label: 'Gàidhlig', value: 'gd' }, + { label: 'Galego', value: 'gl' }, + { label: '𐌲𐌿𐍄𐌹𐍃𐌺𐌰', value: 'got' }, + { label: 'Creolese', value: 'gyn' }, + { label: '中文-客家话', value: 'hak' }, + { label: '한국어', value: 'ko' }, + { label: 'Hausa | هَرْشَن هَوْسَ', value: 'hau' }, + { label: 'हिन्दी', value: 'hi' }, + { label: 'Hrvatski', value: 'hr' }, + { label: 'ʻŌlelo Hawaiʻi', value: 'haw' }, + { label: 'Interlingua', value: 'ia' }, + { label: 'isiZulu', value: 'zu' }, + { label: 'Íslenska', value: 'is' }, + { label: 'Italiano', value: 'it' }, + { label: 'עברית', value: 'he' }, + { label: 'Kalaallisut', value: 'kal' }, + { label: 'ಕನ್ನಡ', value: 'kan' }, + { label: 'ქართული', value: 'kat' }, + { label: 'Kernewek', value: 'cor' }, + { label: 'ភាសាខ្មែរ', value: 'khm' }, + { label: 'Khuzdul', value: 'qkz' }, + { label: 'Kiswahili', value: 'sw' }, + { label: 'kreyòl ayisyen', value: 'ht' }, + { label: 'Kurdî | کوردی', value: 'ku' }, + { label: 'Кыргызча', value: 'kir' }, + { label: 'Langue des signes québécoise', value: 'fcs' }, + { label: 'Latviešu valoda', value: 'lv' }, + { label: 'Lëtzebuergesch', value: 'lb' }, + { label: 'Lietuvių kalba', value: 'lt' }, + { label: 'Lingua latina', value: 'la' }, + { label: 'Magyar', value: 'hu' }, + { label: 'македонски', value: 'mk' }, + { label: 'മലയാളം', value: 'ml' }, + { label: 'Malti', value: 'mt' }, + { label: 'ᠮᠠᠨᠵᡠ ᡤᡳᠰᡠᠨ', value: 'mnc' }, + { label: "Mando'a", value: 'qmd' }, + { label: 'मराठी', value: 'mr' }, + { label: 'Mikisúkî', value: 'mik' }, + { label: 'ᠮᠣᠩᠭᠣᠯ ᠪᠢᠴᠢᠭ᠌ | Монгол Кирилл үсэг', value: 'mon' }, + { label: 'မြန်မာဘာသာ', value: 'my' }, + { label: 'Эрзянь кель', value: 'myv' }, + { label: 'Nāhuatl', value: 'nah' }, + { label: '中文-闽南话 臺語', value: 'nan' }, + { label: 'Nawat', value: 'ppl' }, + { label: 'Nederlands', value: 'nl' }, + { label: '日本語', value: 'ja' }, + { label: 'Norsk', value: 'no' }, + { label: 'Азәрбајҹан дили | آذربایجان دیلی', value: 'azj' }, + { label: 'Нохчийн мотт', value: 'ce' }, + { label: '‘O’odham Ñiok', value: 'ood' }, + { label: 'لسان عثمانى', value: 'ota' }, + { label: 'پښتو', value: 'ps' }, + { label: 'Plattdüütsch', value: 'nds' }, + { label: 'Polski', value: 'pl' }, + { label: 'Português brasileiro', value: 'ptBR' }, + { label: 'Português europeu', value: 'ptPT' }, + { label: 'ਪੰਜਾਬੀ', value: 'pa' }, + { label: 'qazaqşa | қазақша', value: 'kaz' }, + { label: 'Uncategorized Constructed Languages', value: 'qlq' }, + { label: 'Quenya', value: 'qya' }, + { label: 'Română', value: 'ro' }, + { label: 'Русский', value: 'ru' }, + { label: 'Scots', value: 'sco' }, + { label: 'Shqip', value: 'sq' }, + { label: 'Sindarin', value: 'sjn' }, + { label: 'සිංහල', value: 'si' }, + { label: 'Slovenčina', value: 'sk' }, + { label: 'Slovenščina', value: 'slv' }, + { label: 'Sprēkō Þiudiskō', value: 'gem' }, + { label: 'Српски', value: 'sr' }, + { label: 'suomi', value: 'fi' }, + { label: 'Svenska', value: 'sv' }, + { label: 'தமிழ்', value: 'ta' }, + { label: 'татар теле', value: 'tat' }, + { label: 'te reo Māori', value: 'mri' }, + { label: 'తెలుగు', value: 'tel' }, + { label: 'ไทย', value: 'th' }, + { label: 'Thermian', value: 'tqx' }, + { label: 'བོད་སྐད་', value: 'bod' }, + { label: 'Tiếng Việt', value: 'vi' }, + { label: 'ϯⲙⲉⲧⲣⲉⲙⲛ̀ⲭⲏⲙⲓ', value: 'cop' }, + { label: 'tlhIngan-Hol', value: 'tlh' }, + { label: 'toki pona', value: 'tok' }, + { label: 'Trinidadian Creole', value: 'trf' }, + { label: 'τσακώνικα', value: 'tsd' }, + { label: 'ᏣᎳᎩ ᎦᏬᏂᎯᏍᏗ', value: 'chr' }, + { label: 'Türkçe', value: 'tr' }, + { label: 'Українська', value: 'uk' }, + { label: 'اُردُو', value: 'urd' }, + { label: 'ئۇيغۇر تىلى', value: 'uig' }, + { label: 'Volapük', value: 'vol' }, + { label: '中文-吴语', value: 'wuu' }, + { label: 'יידיש', value: 'yi' }, + { label: 'maayaʼ tʼàan', value: 'yua' }, + { label: '中文-广东话 粵語', value: 'yue' }, + { label: '中文-普通话 國語', value: 'zh' }, + ], + type: FilterTypes.Picker, + }, + completion: { + value: '', + label: 'Completion Status', + options: [ + { label: 'All works', value: 'checked' }, + { label: 'Complete works only', value: 'T' }, + { label: 'Works in progress only', value: 'F' }, + ], + type: FilterTypes.Picker, + }, + crossover: { + value: '', + label: 'Crossover Status', + options: [ + { label: 'Include crossovers', value: 'checked' }, + { label: 'Exclude crossovers', value: 'T' }, + { label: 'Only crossovers', value: 'F' }, + ], + type: FilterTypes.Picker, + }, + categories: { + value: [], + label: 'Categories', + options: [ + { label: 'F/F', value: '116' }, + { label: 'F/M', value: '22' }, + { label: 'Gen', value: '21' }, + { label: 'M/M', value: '23' }, + { label: 'Multi', value: '2246' }, + { label: 'Other', value: '24' }, + ], + type: FilterTypes.CheckboxGroup, + }, + warningsFilter: { + value: [], + label: 'Warnings', + options: [ + { label: 'Creator Chose Not To Use Archive Warnings', value: '14' }, + { label: 'Graphic Depictions Of Violence', value: '17' }, + { label: 'Major Character Death', value: '18' }, + { label: 'No Archive Warnings Apply', value: '16' }, + { label: 'Rape/Non-Con', value: '19' }, + { label: 'Underage', value: '20' }, + ], + type: FilterTypes.CheckboxGroup, + }, + singlechap: { + value: false, + label: 'Single Chapter Stories', + type: FilterTypes.Switch, + }, + author: { + value: '', + label: 'Author/Artist', + type: FilterTypes.TextInput, + }, + dateFilter: { + value: '', + label: 'Enter single Number only Date', + type: FilterTypes.TextInput, + }, + dateIncrements: { + value: 'days+ago', + label: 'Must choose date type', + options: [ + { label: 'Days', value: 'days+ago' }, + { label: 'Weeks', value: 'weeks+ago' }, + { label: 'Months', value: 'months+ago' }, + { label: 'Years', value: 'years+ago' }, + ], + type: FilterTypes.Picker, + }, + words: { + value: '', + label: + 'Word Count, exact number eg. 40 or less than eg. <40 or greater than eg. >40 or range eg. 10-100', + type: FilterTypes.TextInput, + }, + hits: { + value: '', + label: 'Hits', + type: FilterTypes.TextInput, + }, + bookmarks: { + value: '', + label: 'Bookmarks', + type: FilterTypes.TextInput, + }, + comments: { + value: '', + label: 'Comments', + type: FilterTypes.TextInput, + }, + kudos: { + value: '', + label: 'Kudos', + type: FilterTypes.TextInput, + }, + } satisfies Filters; +} + +export default new ArchiveOfOurOwn(); diff --git a/plugins/english/bestlightnovel.broken.ts b/plugins/english/bestlightnovel.broken.ts new file mode 100644 index 000000000..2eda9b929 --- /dev/null +++ b/plugins/english/bestlightnovel.broken.ts @@ -0,0 +1,272 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; + +class BLN implements Plugin.PluginBase { + id = 'BLN'; + name = 'BestLightNovel'; + icon = 'src/en/bestlightnovel/icon.png'; + site = 'https://bestlightnovel.com/'; + version = '1.0.1'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.update_item.list_category').each((i, el) => { + const novelUrl = loadedCheerio(el).find('h3 > a').attr('href'); + if (!novelUrl) { + // TODO: Handle error + console.error('No novel url!'); + return; + } + + const novelName = loadedCheerio(el).find('h3 > a').text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + + const novel = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site + 'novel_list?'; + link += 'type=' + filters.type.value; + link += '&category=' + filters.category.value; + link += '&state=' + filters.status.value; + link += '&page=' + page; + + const result = await fetchApi(link); + if (!result.ok) { + console.error(await result.text()); + // TODO: Cloudflare protection or other error + return []; + } + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.truyen_info_right h1').text().trim() || '', + cover: loadedCheerio('.info_image img').attr('src') || defaultCover, + summary: loadedCheerio('#noidungm').text()?.trim() || '', + chapters: [], + }; + loadedCheerio('ul.truyen_info_right > li').each(function () { + const detailName = loadedCheerio(this).find('span').text(); + const detail = loadedCheerio(this) + .find('a') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(', '); + + switch (detailName) { + case 'Author(s): ': + novel.author = detail; + break; + case 'GENRES: ': + novel.genres = detail; + break; + case 'STATUS : ': + novel.status = + detail === 'Ongoing' + ? NovelStatus.Ongoing + : detail === 'Completed' + ? NovelStatus.Completed + : NovelStatus.Unknown; + } + }); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.chapter-list div.row').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const releaseDate = loadedCheerio(el).find('span:last').text().trim(); + + const months = [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + ].join('|'); + const rx = new RegExp(`(${months})-(\\d{1,2})-(\\d{2})`, 'i').exec( + releaseDate, + ); + if (!rx) return; + const year = 2000 + +rx[3]; + const month = months.indexOf(rx[1].toLowerCase()); + const day = +rx[2]; + + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) { + // TODO: Handle error + console.error('No chapter url!'); + return; + } + + chapter.push({ + name: chapterName, + releaseTime: new Date(year, month, day).toISOString(), + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#vung_doc').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const url = `${this.site}search_novels/${encodeURIComponent(searchTerm)}?page=${page}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + status: { + label: 'Status', + value: 'all', + options: [ + { label: 'ALL', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: 'topview', + label: 'Type', + options: [ + { label: 'Recently updated', value: 'latest' }, + { label: 'Newest', value: 'newest' }, + { label: 'Top view', value: 'topview' }, + ], + type: FilterTypes.Picker, + }, + category: { + label: 'Category', + value: 'all', + options: [ + { label: 'ALL', value: 'all' }, + { label: 'Action', value: '1' }, + { label: 'Adventure', value: '2' }, + { label: 'Animals', value: '65' }, + { label: 'Arts', value: '40' }, + { label: 'Biographies', value: '41' }, + { label: 'Business', value: '42' }, + { label: 'Chinese', value: '3' }, + { label: 'Comedy', value: '4' }, + { label: 'Computers', value: '43' }, + { label: 'Crafts, Hobbies', value: '45' }, + { label: 'Drama', value: '5' }, + { label: 'Education', value: '46' }, + { label: 'English', value: '6' }, + { label: 'Entertainment', value: '47' }, + { label: 'Fantasy', value: '7' }, + { label: 'Fiction', value: '48' }, + { label: 'Gender Bender', value: '8' }, + { label: 'Harem', value: '9' }, + { label: 'Historical', value: '10' }, + { label: 'History', value: '49' }, + { label: 'Home', value: '50' }, + { label: 'Horror', value: '11' }, + { label: 'Humor', value: '51' }, + { label: 'Investing', value: '52' }, + { label: 'Josei', value: '12' }, + { label: 'Korean', value: '13' }, + { label: 'Literature', value: '53' }, + { label: 'Lolicon', value: '14' }, + { label: 'Martial Arts', value: '15' }, + { label: 'Mature', value: '16' }, + { label: 'Mecha', value: '17' }, + { label: 'Memoirs', value: '54' }, + { label: 'Mystery', value: '18' }, + { label: 'Original', value: '19' }, + { label: 'Other Books', value: '66' }, + { label: 'Philosophy', value: '55' }, + { label: 'Photography', value: '56' }, + { label: 'Politics', value: '57' }, + { label: 'Professional', value: '58' }, + { label: 'Psychological', value: '20' }, + { label: 'Reference', value: '59' }, + { label: 'Reincarnation', value: '21' }, + { label: 'Religion', value: '60' }, + { label: 'Romance', value: '22' }, + { label: 'School Life', value: '23' }, + { label: 'School Stories', value: '67' }, + { label: 'Sci-Fi', value: '24' }, + { label: 'Seinen', value: '25' }, + { label: 'Short Stories', value: '68' }, + { label: 'Shotacon', value: '26' }, + { label: 'Shoujo', value: '27' }, + { label: 'Shoujo Ai', value: '28' }, + { label: 'Shounen', value: '29' }, + { label: 'Shounen Ai', value: '30' }, + { label: 'Slice Of Life', value: '31' }, + { label: 'Smut', value: '32' }, + { label: 'Social Science', value: '61' }, + { label: 'Spirituality', value: '62' }, + { label: 'Sports', value: '33' }, + { label: 'Supernatural', value: '34' }, + { label: 'Teaser', value: '69' }, + { label: 'Technical', value: '63' }, + { label: 'Technology', value: '64' }, + { label: 'Tragedy', value: '35' }, + { label: 'Virtual Reality', value: '36' }, + { label: 'Wuxia', value: '37' }, + { label: 'Xianxia', value: '38' }, + { label: 'Xuanhuan', value: '39' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new BLN(); diff --git a/plugins/english/chrysanthemumgarden.ts b/plugins/english/chrysanthemumgarden.ts new file mode 100644 index 000000000..2f6ec9fb4 --- /dev/null +++ b/plugins/english/chrysanthemumgarden.ts @@ -0,0 +1,158 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; + +class Chrysanthemumgarden implements Plugin.PluginBase { + id = 'chrysanthemumgarden'; + name = 'Chrysanthemum Garden'; + icon = 'src/en/chrysanthemumgarden/icon.png'; + site = 'https://chrysanthemumgarden.com'; + version = '1.0.3'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions, + ): Promise { + const req = await fetchApi( + this.site + (pageNo === 1 ? '/books' : '/books/page/' + pageNo) + '/', + ); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + return loadedCheerio('article') + .map((i, el) => { + if ( + loadedCheerio(el) + .find('div.series-genres > a') + .text() + .includes('Manhua') + ) + return; + const href = loadedCheerio(el).find('h2.novel-title > a').attr('href'); + if (!href) return; + return { + name: loadedCheerio(el).find('h2.novel-title > a').text(), + path: href + .replace(this.site, '') + .replace(/^\//, '') + .replace(/\/$/, ''), + cover: + loadedCheerio(el) + .find('div.novel-cover > img') + .attr('data-breeze') || defaultCover, + }; + }) + .toArray() + .filter(Boolean); + } + + async parseNovel(novelPath: string): Promise { + const req = await fetchApi(this.site + '/' + novelPath + '/'); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + loadedCheerio('h1.novel-title > span.novel-raw-title').remove(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.novel-title').text(), + cover: loadedCheerio('div.novel-cover > img').attr('data-breeze'), + summary: loadedCheerio('div.entry-content > p') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join('\n\n'), + }; + + const novelInfoHtml = loadedCheerio('div.novel-info').html(); + novel.author = novelInfoHtml + ? novelInfoHtml.match(/Author:\s*([^<]*)
/)?.[1]?.trim() + : undefined; + novel.genres = [ + ...loadedCheerio('div.series-genres > a') + .map((i, el) => loadedCheerio(el).text()) + .toArray(), + ...loadedCheerio('a.series-tag') + .map((i, el) => loadedCheerio(el).text().split('(')[0].trim()) + .toArray(), + ].join(', '); + + novel.chapters = loadedCheerio('div.chapter-item > a') + .map((i, el) => { + const href = loadedCheerio(el).attr('href'); + if (!href) return; + return { + name: loadedCheerio(el).text().trim(), + path: href + .replace(this.site, '') + .replace(/^\//, '') + .replace(/\/$/, ''), + }; + }) + .toArray(); + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const req = await fetchApi(this.site + '/' + chapterPath + '/'); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + return loadedCheerio('div#novel-content').html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo !== 1) return []; + + const req = await fetchApi(this.site + '/wp-json/cg/novels'); + const body = await req.json(); + + const allNovels: ChrysanthemumGardenNovelItem[] = ( + body as ChrysanthemumGardenNovel[] + ).map( + (novel: ChrysanthemumGardenNovel): ChrysanthemumGardenNovelItem => ({ + name: novel.name, + path: novel.link + .replace(this.site, '') + .replace(/\/$/, '') + .replace(/^\//, ''), + cover: defaultCover, + }), + ); + + if (!allNovels) return []; + + return allNovels.filter(novel => + novel.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + async getAllNovels() { + // linter? what is the purpose of this + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/chapter/') + path; +} + +export default new Chrysanthemumgarden(); + +type ChrysanthemumGardenNovel = { + name: string; + link: string; +}; + +type ChrysanthemumGardenNovelItem = { + name: string; + path: string; + cover: string; +}; diff --git a/plugins/english/crimsonscrolls.ts b/plugins/english/crimsonscrolls.ts new file mode 100644 index 000000000..41bb9b810 --- /dev/null +++ b/plugins/english/crimsonscrolls.ts @@ -0,0 +1,215 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { storage } from '@libs/storage'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +enum APIAction { + novels = 'load_novels', + search = 'live_novel_search', +} + +type APIParams = { + action: APIAction; + params: Record; +}; + +type ChapterJSON = { + items: ChapterItem[]; + total: number; + total_pages?: number; + page?: number; + per_page?: number; + order?: string; +}; + +type ChapterItem = { + id: number; + title: string; + url: string; + locked: boolean; +}; + +class CrimsonScrollsPlugin implements Plugin.PluginBase { + id = 'crimsonscrolls'; + name = 'Crimson Scrolls'; + icon = 'src/en/crimsonscrolls/icon.png'; + site = 'https://crimsonscrolls.net'; + version = '1.0.1'; + + hideLocked = storage.get('hideLocked'); + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + async queryAPI(query: APIParams): Promise { + const formData = new FormData(); + formData.append('action', query.action); + for (const [key, value] of Object.entries(query.params)) + formData.append(key, value.toString()); + + const result = await fetchApi(`${this.site}/wp-admin/admin-ajax.php`, { + method: 'POST', + body: formData, + }).then(result => result.json()); + + return parseHTML(result.html); + } + + async fetchChapters( + id: number, + page?: number | undefined, + ): Promise { + const url = `${this.site}/wp-json/cs/v1/novels/${id}/chapters?per_page=75&order=asc`; + const data: ChapterJSON = await fetchApi(`${url}&page=${page ?? 1}`).then( + r => r.json(), + ); + + const items = data.items || []; + const locked = items.some(e => e.locked); + + if ( + data.total_pages && + (data.page ?? 1) < data.total_pages && + !(locked && this.hideLocked) + ) { + const nextItems = await this.fetchChapters(id, (data.page ?? 0) + 1); + return items.concat(nextItems); + } + + return items; + } + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(':is(a.live-search-item, div.novel-list-card)').each( + (i, el) => { + const novelName = loadedCheerio(el) + .find(':is(div.live-search-title, h3.novel-title)') + .text() + .trim(); + const novelCover = loadedCheerio(el) + .find(':is(img.live-search-cover, div.novel-cover img)') + .attr('src'); + const novelUrl = + loadedCheerio(el).find('a').attr('href') || + loadedCheerio(el).attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName + .trim() + .split(' ') + .filter(e => e.length > 0) + .join(' '), + cover: novelCover, + path: novelUrl + ? new URL(novelUrl, this.site).pathname.substring(1) + : defaultCover, + }; + novels.push(novel); + }, + ); + return novels; + } + + async popularNovels(page: number): Promise { + const loadedCheerio = await this.queryAPI({ + action: APIAction.novels, + params: { page: page.toString() }, + }); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise { + const result = await fetchApi(`${this.site}/${novelPath}`).then(r => + r.text(), + ); + + const loadedCheerio = parseHTML(result); + const novelInfo = loadedCheerio('#single-novel-content-wrapper'); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: novelInfo.find('h1').text().trim() ?? 'Untitled', + cover: + novelInfo.find('img:first').data('src')?.toString() ?? defaultCover, + summary: novelInfo.find('#synopsis-full').text().trim(), + author: novelInfo.find('strong:first').next().text().trim(), + chapters: [], + }; + + novel.genres = novelInfo + .find('.cs-genre-chip') + .map((_, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(','); + + const rawStatus = novelInfo.find('.cs-nsb-badge').text().trim(); + const map: Record = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + }; + novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown; + + const id = loadedCheerio('#chapter-list').data('novel'); + const chapters = await this.fetchChapters(Number(id)); + + const novelChapters: Plugin.ChapterItem[] = []; + chapters.forEach((chapter, index) => { + if (!(chapter.locked && this.hideLocked)) { + novelChapters.push({ + name: chapter.locked ? `🔒 ${chapter.title}` : chapter.title, + path: chapter.url + ? new URL(chapter.url, this.site).pathname.split('/')[2] + : '', + chapterNumber: index + 1, + }); + } + }); + novel.chapters = novelChapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(`${this.site}/chapter/${chapterPath}`).then(r => + r.text(), + ); + const loadedCheerio = parseHTML(body); + for (const i of [ + 'hr.cs-attrib-divider', + 'div.cs-attrib', + 'p.cs-chapter-attrib', + ]) + loadedCheerio(`#chapter-display ${i}:last`).remove(); + + const chapterText = loadedCheerio('#chapter-display').html() || ''; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise { + const loadedCheerio = await this.queryAPI({ + action: APIAction.search, + params: { query: searchTerm }, + }); + + return this.parseNovels(loadedCheerio); + } + + // not sure purpose of this, commented out + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/novel/' + path; +} + +export default new CrimsonScrollsPlugin(); diff --git a/plugins/english/divinedaolibrary.ts b/plugins/english/divinedaolibrary.ts new file mode 100644 index 000000000..9c3e5447e --- /dev/null +++ b/plugins/english/divinedaolibrary.ts @@ -0,0 +1,298 @@ +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes, FilterValueWithType } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; + +// TODO: change layoput, layout has updated + +class DDLPlugin implements Plugin.PluginBase { + id = 'DDL.com'; + name = 'Divine Dao Library'; + site = 'https://www.divinedaolibrary.com/'; + version = '1.1.1'; + icon = 'src/en/divinedaolibrary/icon.png'; + + filters = { + category: { + type: FilterTypes.CheckboxGroup, + label: 'State', + value: ['Completed', 'Translating', 'Lost in Voting Poll', 'Dropped'], + options: [ + { label: 'Completed', value: 'Completed' }, + { label: 'Translating', value: 'Translating' }, + { label: 'Lost in Voting Poll', value: 'Lost in Voting Poll' }, + { label: 'Dropped', value: 'Dropped' }, + { label: 'Personally Written', value: 'Personally Written' }, + ], + }, + } satisfies Filters; + + allNovelsCache: readonly (readonly [string, string, string])[] | undefined; + novelItemCache = new Map>(); + + /** + * Safely extract the pathname from any URL on {@link site}. Check the root + * site as there are novels linking off-site (to Patreon). + * + * @private + */ + getPath(url: string): string | undefined { + if (!url.startsWith(this.site)) { + return undefined; + } + const trimmed = url.substring(this.site.length).replace(/(^\/+|\/+$)/g, ''); + if (trimmed.length === 0) { + return undefined; + } + return trimmed; + } + + /** + * Map an array with an asynchronous function and only return the array items + * that successfully were fulfilled with values other than undefined. + * + * @private + */ + async asyncMap( + collection: readonly T[], + callbackfn: (value: T, index: number, array: readonly T[]) => Promise, + ): Promise[]> { + return (await Promise.allSettled(collection.map(callbackfn))) + .filter( + ( + p: PromiseSettledResult, + ): p is PromiseFulfilledResult> => + p.status === 'fulfilled' && p.value !== undefined, + ) + .map(({ value }) => value); + } + + /** + * DDL links to future (unpublished) chapters from its novel pages. To be + * able to report updates correctly, try to figure out which chapter was the + * actual latest one to be published. + * + * @private + * @returns the path value of the latest published chapter (or undefined) + */ + async findLatestChapter(novelPath: string): Promise { + const link = `${this.site}wp-json/wp/v2/categories?slug=${novelPath}`; + const guessCategory = await fetchApi(link).then(res => res.json()); + if (guessCategory.length !== 1) { + return undefined; + } + const categoryId = guessCategory[0].id; + const chapterLink = `${this.site}wp-json/wp/v2/posts?categories=${categoryId}&per_page=1`; + const lastChapter = await fetchApi(chapterLink).then(res => res.json()); + if (lastChapter.length !== 1) { + return undefined; + } + return lastChapter[0].slug; + } + + /** + * Based on the path, grab all the available information about a novel. Used + * to seed the {@link novelItemCache} as well as to fetch the actual single + * novel views with chapter list. + * + * @private + */ + async grabNovel( + path: string, + getChapters = false, + ): Promise<(Plugin.SourceNovel & Required) | undefined> { + const link = `${this.site}wp-json/wp/v2/pages?slug=${path}`; + const data = await fetchApi(link).then(res => res.json()); + if (data.length !== 1) { + return undefined; + } + const content = parseHTML(data[0].content.rendered); + const excerpt = parseHTML(data[0].excerpt.rendered); + const image = content('img').first(); + let chapters: Plugin.ChapterItem[] = []; + if (getChapters) { + const linkedChapters = content('li > span > a') + .map((_, anchorEl) => { + const chapterPath = this.getPath(anchorEl.attribs['href']); + if (!chapterPath) return; + return { + name: content(anchorEl).text(), + path: chapterPath, + } satisfies Plugin.ChapterItem; + }) + .toArray(); + const lastChapterPath = await this.findLatestChapter(path); + if (lastChapterPath) { + chapters = linkedChapters.slice( + 0, + 1 + + linkedChapters.findIndex( + chapter => chapter.path === lastChapterPath, + ), + ); + } else { + chapters = linkedChapters; + } + } + return { + name: data[0].title.rendered, + path, + cover: image.attr('data-lazy-src') ?? image.attr('src') ?? defaultCover, + author: content('h3') + .first() + .text() + .replace(/^Author:\s*/g, ''), + summary: excerpt('p') + .first() + .text() + .replace(/^.+Description\s*/g, ''), + chapters, + }; + } + + /** + * Based on the path, grab the {@link Plugin.NovelItem} information from the + * cache. If not available in the cache, it will fetch the information. + * + * @private + */ + async grabCachedNovel(path: string): Promise { + const fromCache = this.novelItemCache.get(path); + if (fromCache) { + return fromCache; + } + const sourceNovel = await this.grabNovel(path, false); + if (sourceNovel === undefined) { + return undefined; + } + const novel = { + name: sourceNovel.name, + path: sourceNovel.path, + cover: sourceNovel.cover, + }; + this.novelItemCache.set(path, novel); + return novel; + } + + /** + * Get the list of all novels listed on the novels page. Cache it so filters + * do not have to refetch the same HTML again. + * + * @private + */ + async grabCachedNovels(): Promise< + readonly (readonly [string, string, string])[] | undefined + > { + if (this.allNovelsCache) { + return this.allNovelsCache; + } + const body = await fetchApi(this.site + 'novels').then(res => res.text()); + const loadedCheerio = parseHTML(body); + const novels = loadedCheerio('.entry-content ul') + .map((_, listEl) => { + const list = loadedCheerio(listEl); + const category = list.prev().text(); + return list + .find('a') + .map((_, anchorEl) => { + const path = this.getPath(anchorEl.attribs['href']); + if (!path) return; + const name = loadedCheerio(anchorEl).text(); + return [[category, name, path]] as const; + }) + .toArray(); + }) + .toArray(); + this.allNovelsCache = novels; + return novels; + } + + /** + * Parse list of paths of recently updated novels from the homepage. + * + * @private + */ + async latestNovels(): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const novelPaths = loadedCheerio('#main') + .find('a[rel="category tag"]') + .map((_, anchorEl) => { + const path = this.getPath(anchorEl.attribs['href']); + return path; + }) + .toArray(); + const uniqueNovelPaths = new Set(novelPaths); + return Array.from(uniqueNovelPaths); + } + + /** + * Parse list of paths from novels list, optionally filtered. + * + * @private + */ + async allNovels( + categoryFilter: FilterValueWithType, + ) { + const allNovels = await this.grabCachedNovels(); + if (!allNovels) return []; + return allNovels + .filter(([category]) => categoryFilter.value.includes(category)) + .map(([, , path]) => path); + } + + async popularNovels( + pageNo: number, + options: Plugin.PopularNovelsOptions, + ): Promise { + if (pageNo !== 1) { + return []; + } + const novelsList = options.showLatestNovels + ? this.latestNovels() + : this.allNovels(options.filters.category); + return await this.asyncMap( + await novelsList, + this.grabCachedNovel.bind(this), + ); + } + + async parseNovel(novelPath: string): Promise { + const sourceNovel = await this.grabNovel(novelPath, true); + if (sourceNovel === undefined) { + throw new Error(`The path "${novelPath}" could not be resolved.`); + } + return sourceNovel; + } + + async parseChapter(chapterPath: string): Promise { + const chapterLink = `${this.site}wp-json/wp/v2/posts?slug=${chapterPath}`; + const chapter = await fetchApi(chapterLink).then(res => res.json()); + if (chapter.length !== 1) { + return ''; + } + const title = `

${chapter[0].title.rendered}

`; + const content = chapter[0].content.rendered; + return `${title}${content}`; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + if (pageNo !== 1) { + return []; + } + const allNovels = await this.grabCachedNovels(); + if (!allNovels) return []; + const foundNovels = allNovels + .filter(([, name]) => + name.toLocaleLowerCase().includes(searchTerm.toLocaleLowerCase()), + ) + .map(([, , path]) => path); + return await this.asyncMap(foundNovels, this.grabCachedNovel.bind(this)); + } +} + +export default new DDLPlugin(); diff --git a/plugins/english/dreambigtl.ts b/plugins/english/dreambigtl.ts new file mode 100644 index 000000000..3f0661e42 --- /dev/null +++ b/plugins/english/dreambigtl.ts @@ -0,0 +1,188 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +class DreamBigTL implements Plugin.PluginBase { + id = 'dreambigtl'; + name = 'Dream Big Translations'; + version = '1.0.0'; + site = 'https://www.dreambigtl.com/'; + icon = 'src/en/dreambigtl/icon.png'; + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise { + const url = `${this.site}p/disclaimer.html`; + + const response = await fetchApi(url); + const body = await response.text(); + const loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + const categories = ['New Novels', 'Ongoing Novels', 'Completed Novels']; + + loadedCheerio('#webify-pro-main-nav-menu > li.has-sub').each( + (_, categoryElem) => { + const categoryName = loadedCheerio(categoryElem) + .children('a') + .first() + .text() + .trim(); + + if (categories.includes(categoryName)) { + const subMenu = loadedCheerio(categoryElem).find('ul.sub-menu.m-sub'); + subMenu.find('li > a').each((_, novelElem) => { + const novelName = loadedCheerio(novelElem).text().trim(); + const novelUrl = loadedCheerio(novelElem).attr('href'); + + if (novelUrl) { + novels.push({ + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: '', + }); + } + }); + } + }, + ); + + novels.forEach(novel => console.log('Novel:', novel.name, novel.path)); + + if (novels.length === 0) { + // Fallback: try to parse everything... + loadedCheerio('a').each((_, elem) => { + const href = loadedCheerio(elem).attr('href'); + if (href && href.includes('/p/') && !href.includes('disclaimer')) { + const novelName = loadedCheerio(elem).text().trim(); + novels.push({ + name: novelName, + path: href.replace(this.site, ''), + cover: '', + }); + } + }); + } + + if (showLatestNovels) { + novels.sort((a, b) => b.path.localeCompare(a.path)); + } + + return novels; + } + + async parseNovel(novelPath: string): Promise { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title').text().trim(), + cover: loadedCheerio('.post-body img').first().attr('src') || '', + summary: loadedCheerio('.post-body p').first().text().trim(), + author: + loadedCheerio('.post-body') + .text() + .match(/Author:\s*(.+)/i)?.[1] + ?.trim() || 'Unknown', + status: this.getNovelStatus(novelPath), + chapters: await this.parseChapters(loadedCheerio), + }; + + return novel; + } + + async parseChapters( + loadedCheerio: CheerioAPI, + ): Promise { + const chapters: Plugin.ChapterItem[] = []; + // Parse "Free Tier" chapters + loadedCheerio('.chapter-panel').each((_, panel) => { + const panelTitle = loadedCheerio(panel).find('summary').text().trim(); + if (panelTitle === 'Free Tier') { + loadedCheerio(panel) + .find('ul li a') + .each((_, chapterEle) => { + const chapterName = loadedCheerio(chapterEle).text().trim(); + const chapterUrl = loadedCheerio(chapterEle).attr('href'); + if (chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + } + }); + + // Parse "List of Chapters" + if (chapters.length === 0) { + loadedCheerio( + 'h2:contains("List of Chapters"), span:contains("List of Chapters")', + ).each((_, header) => { + const chapterList = loadedCheerio(header).next('ul'); + chapterList.find('li a').each((_, chapterEle) => { + const chapterName = loadedCheerio(chapterEle).text().trim(); + const chapterUrl = loadedCheerio(chapterEle).attr('href'); + if (chapterUrl) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + }); + } + + return chapters.reverse(); + } + + getNovelStatus(novelPath: string): string { + if (novelPath.includes('completed')) return 'Completed'; + return 'Ongoing'; + } + + async parseChapter(chapterPath: string): Promise { + const url = this.site + chapterPath; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const chapterTitle = loadedCheerio('h1.entry-title').text().trim(); + const chapterContent = loadedCheerio('.post-body').html() || ''; + + return `

${chapterTitle}

${chapterContent}`; + } + + async searchNovels(searchTerm: string): Promise { + const url = `${this.site}search?q=${encodeURIComponent(searchTerm)}`; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio( + '.blog-posts.index-post-wrap .blog-post.hentry.index-post', + ).each((_, ele) => { + const novelName = loadedCheerio(ele).find('.entry-title a').text().trim(); + const novelUrl = loadedCheerio(ele).find('.entry-title a').attr('href'); + const novelCover = + loadedCheerio(ele).find('.entry-image').attr('data-image') || ''; + + if (novelUrl) { + novels.push({ + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }); + } + }); + + return novels; + } +} + +export default new DreamBigTL(); diff --git a/plugins/english/earlynovel.broken.ts b/plugins/english/earlynovel.broken.ts new file mode 100644 index 000000000..0017c838c --- /dev/null +++ b/plugins/english/earlynovel.broken.ts @@ -0,0 +1,199 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; + +class EarlyNovelPlugin implements Plugin.PagePlugin { + id = 'earlynovel'; + name = 'Early Novel'; + version = '1.0.1'; + icon = 'src/en/earlynovel/icon.png'; + site = 'https://earlynovel.net/'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.col-truyen-main > .list-truyen > .row').each((i, el) => { + const novelUrl = loadedCheerio(el) + .find('h3.truyen-title > a') + .attr('href'); + const novelName = loadedCheerio(el).find('h3.truyen-title > a').text(); + const novelCover = loadedCheerio(el).find('.lazyimg').attr('data-image'); + + if (!novelUrl) return; + novels.push({ + path: novelUrl, + name: novelName, + cover: novelCover, + }); + }); + return novels; + } + + parseChapters(loadedCheerio: CheerioAPI) { + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('ul.list-chapter > li').each((i, el) => { + const chapterName = loadedCheerio(el).find('.chapter-text').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href')?.slice(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + }); + }); + return chapter; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise { + let link = this.site; + + if (filters.genres.value.length) link += filters.genres.value; + else link += filters.order.value; + + link += `?page=${pageNo}`; + + const body = await fetchApi(link).then((res: Response) => res.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel( + novelPath: string, + ): Promise { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + loadedCheerio('.glyphicon-menu-right').closest('li').remove(); + const pagenav = loadedCheerio('.page-nav').prev().find('a'); + const lastPageStr = pagenav.attr('title')?.match(/(\d+)/); + const lastPage = Number(lastPageStr?.[1] || '0'); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.book > img').attr('alt') || 'Untitled', + cover: loadedCheerio('.book > img').attr('src'), + summary: loadedCheerio('.desc-text').text().trim(), + chapters: [], + totalPages: lastPage, + }; + + loadedCheerio('.info > div > h3').each(function () { + const detailName = loadedCheerio(this).text(); + const detail = loadedCheerio(this) + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + switch (detailName) { + case 'Author:': + novel.author = detail; + break; + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); + + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + + async parsePage(novelPath: string, page: string): Promise { + const url = this.site + novelPath + '?page=' + page; + const body = await fetchApi(url).then((res: Response) => res.text()); + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { chapters }; + } + + async parseChapter(chapterPath: string): Promise { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chapter-c').html() || ''; + + return chapterText; + } + + async searchNovels(searchTerm: string): Promise { + const url = `${this.site}search?keyword=${encodeURIComponent(searchTerm)}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + order: { + value: '/most-popular', + label: 'Order by', + options: [ + { label: 'Latest Release', value: '/latest-release-novel' }, + { label: 'Hot Novel', value: '/hot-novel' }, + { label: 'Completed Novel', value: '/completed-novel' }, + { label: 'Most Popular', value: '/most-popular' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: '', + label: 'Genre', + options: [ + { label: 'None', value: '' }, + { label: 'Action', value: '/genre/action-1' }, + { label: 'Adult', value: '/genre/adult-2' }, + { label: 'Adventure', value: '/genre/adventure-3' }, + { label: 'Comedy', value: '/genre/comedy-4' }, + { label: 'Drama', value: '/genre/drama-5' }, + { label: 'Ecchi', value: '/genre/ecchi-6' }, + { label: 'Fantasy', value: '/genre/fantasy-7' }, + { label: 'Gender Bender', value: '/genre/gender-bender-8' }, + { label: 'Harem', value: '/genre/harem-9' }, + { label: 'Historical', value: '/genre/historical-10' }, + { label: 'Horror', value: '/genre/horror-11' }, + { label: 'Josei', value: '/genre/josei-12' }, + { label: 'Martial Arts', value: '/genre/martial-arts-13' }, + { label: 'Mature', value: '/genre/mature-14' }, + { label: 'Mecha', value: '/genre/mecha-15' }, + { label: 'Mystery', value: '/genre/mystery-16' }, + { label: 'Psychological', value: '/genre/psychological-17' }, + { label: 'Romance', value: '/genre/romance-18' }, + { label: 'School Life', value: '/genre/school-life-19' }, + { label: 'Sci-fi', value: '/genre/sci-fi-20' }, + { label: 'Seinen', value: '/genre/seinen-21' }, + { label: 'Shoujo', value: '/genre/shoujo-22' }, + { label: 'Shoujo Ai', value: '/genre/shoujo-ai-23' }, + { label: 'Shounen', value: '/genre/shounen-24' }, + { label: 'Shounen Ai', value: '/genre/shounen-ai-25' }, + { label: 'Slice of Life', value: '/genre/slice-of-life-26' }, + { label: 'Smut', value: '/genre/smut-27' }, + { label: 'Sports', value: '/genre/sports-28' }, + { label: 'Supernatural', value: '/genre/supernatural-29' }, + { label: 'Tragedy', value: '/genre/tragedy-30' }, + { label: 'Wuxia', value: '/genre/wuxia-31' }, + { label: 'Xianxia', value: '/genre/xianxia-32' }, + { label: 'Xuanhuan', value: '/genre/xuanhuan-33' }, + { label: 'Yaoi', value: '/genre/yaoi-34' }, + { label: 'Yuri', value: '/genre/yuri-35' }, + { label: 'Video Games', value: '/genre/video-games-36' }, + { label: 'Magical Realism', value: '/genre/magical-realism-37' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new EarlyNovelPlugin(); diff --git a/plugins/english/faqwikius.ts b/plugins/english/faqwikius.ts new file mode 100644 index 000000000..e786e1b02 --- /dev/null +++ b/plugins/english/faqwikius.ts @@ -0,0 +1,189 @@ +import { Plugin } from '@/types/plugin'; +import { fetchApi } from '@libs/fetch'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; + +class FaqWikiUs implements Plugin.PluginBase { + id = 'FWK.US'; + name = 'Faq Wiki'; + site = 'https://faqwiki.us/novel'; + version = '3.0.1'; + icon = 'src/en/faqwikius/icon.png'; + + parseNovels(loadedCheerio: CheerioAPI, searchTerm?: string) { + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('.plt-page-item').each((index, element) => { + const name = loadedCheerio(element) + .text() + .replace('Novel – All Chapters', '') + .trim(); + let cover = loadedCheerio(element).find('img').attr('data-ezsrc'); + + // Remove the appended query string + if (cover) { + cover = cover.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + } else { + cover = loadedCheerio(element).find('img').attr('src'); + } + + const path = loadedCheerio(element) + .find('a') + .attr('href') + ?.replace('tp:', 'tps:') + ?.replace(this.site, '') + ?.replace(/\/+$/, ''); + if (!path) return; + + novels.push({ name, cover, path }); + }); + + if (searchTerm) { + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + return novels; + } + + async popularNovels(): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + + async parseNovel(path: string): Promise { + const body = await fetchApi(this.site + path).then(res => res.text()); + const loadedCheerio = parseHTML(body); + loadedCheerio('script').remove(); + + const novel: Plugin.SourceNovel = { + path, + name: '', + }; + + novel.name = loadedCheerio('.entry-title') + .text() + .replace('Novel – All Chapters', '') + .trim(); + + const img = loadedCheerio('.wp-block-image img'); + const cover = img.attr('data-ezsrc') || img.attr('src'); + novel.cover = cover?.replace(/\?ezimgfmt=.*$/, ''); // Regular expression magic! + + const status = loadedCheerio( + "#lcp_instance_0 +:icontains('complete')", + ).text(); + novel.status = status ? NovelStatus.Completed : NovelStatus.Ongoing; + + loadedCheerio('.entry-content strong').each((i, el) => { + const key = loadedCheerio(el).text().trim().toLowerCase(); + const parent = loadedCheerio(el).parent(); + const values = [parent.text().slice(key.length).trim()].concat( + parent + .nextUntil('p:has(strong)') + .map((e, ax) => loadedCheerio(ax).text().trim()) + .get(), + ); + let genreText = values.join(' ').trim(); + const multiWordGenres = [ + //add more when found + 'Slice of Life', + 'School Life', + ]; + multiWordGenres.forEach(genre => { + genreText = genreText.replace( + new RegExp(`\\b${genre}\\b`, 'g'), + genre.replace(/ /g, '_'), + ); + }); + const genres = genreText + .split(/\s+/) + .map(word => word.replace(/_/g, ' ')) + .join(', '); + + switch (key) { + case 'description:': + novel.summary = values.join('\n'); + break; + case 'author(s):': + novel.author = values[0]; + break; + case 'genre:': + novel.genres = genres; + } + }); + + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('.lcp_catlist li a').each((chapterIndex, element) => { + const name = loadedCheerio(element) + .text() + .replace(novel.name + '', '') + .replace('Novel' + '', '') + .trim(); + const path = loadedCheerio(element) + .attr('href') + ?.replace(this.site, '') + ?.replace(/\/+$/, ''); + const chapterNumber = chapterIndex + 1; + if (!path) return; + chapters.push({ + name, + path, + chapterNumber, + }); + }); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + const removal = ['.entry-content span', '.entry-content div', 'script']; + removal.map(e => { + loadedCheerio(e).remove(); + }); + + const chapterText = loadedCheerio('.entry-content').html()!; + // const chapterParagraphs = loadedCheerio('.entry-content p'); + + // let chapterContent; // Save this code in case needed + + // if (chapterParagraphs.length < 5) { + // //some chapter in this site store their whole text in 1-4

, + // chapterContent = chapterParagraphs + // .map((index, element) => { + // const text = loadedCheerio(element).html(); + // return text; + // }) + // .get() + // .join('\n\n'); + // } else { + // // Multiple paragraphs case + // chapterContent = chapterParagraphs + // .map((index, element) => { + // const text = loadedCheerio(element).text().trim(); + // return `

${text}

`; + // }) + // .get() + // .join('\n\n'); + // } + + return chapterText; + } + + async searchNovels(searchTerm: string): Promise { + const body = await fetchApi(this.site).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio, searchTerm); + } +} + +export default new FaqWikiUs(); diff --git a/plugins/english/fenrirrealm.ts b/plugins/english/fenrirrealm.ts new file mode 100644 index 000000000..62a85de76 --- /dev/null +++ b/plugins/english/fenrirrealm.ts @@ -0,0 +1,474 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as loadCheerio } from 'cheerio'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { storage } from '@libs/storage'; +import { defaultCover } from '@libs/defaultCover'; + +type APINovel = { + title: string; + slug: string; + cover: string; + description: string; + status: string; + genres: { name: string }[]; + user?: { + username?: string; + name?: string; + }; +}; + +type APIChapter = { + id: number; + locked: { price: number } | null; + group: null | { + index: number; + slug: string; + }; + title: string; + slug: string; + number: number; + created_at: string; +}; + +type ChapterInfo = { + name: string; + path: string; + releaseTime: string; + chapterNumber: number; +}; + +type Chapter = { + type: string; + content: { + type: string; + attrs?: Attrs; + content: { + type: string; + text?: string; + marks?: { + type: string; + attrs?: Attrs; + }[]; + }[]; + }[]; +}; + +type Attrs = { + textAlign?: string; + href?: string; + level?: number; +}; + +type Nodes = { + type: string; + nodes?: { + type: string; + // couldnt find the sveltekit so disable eslint here + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data: any[]; + }[]; +}; + +class FenrirRealmPlugin implements Plugin.PluginBase { + id = 'fenrir'; + name = 'Fenrir Realm'; + icon = 'src/en/fenrirrealm/icon.png'; + site = 'https://fenrirealm.com'; + version = '1.1.0'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + hideLocked = storage.get('hideLocked'); + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + const params = new URLSearchParams({ + page: pageNo.toString(), + per_page: '20', + status: filters.status.value, + order: showLatestNovels ? 'latest' : filters.sort.value, + }); + filters.genres.value.forEach(g => params.append('genres[]', g)); + + const res = await fetchApi( + `${this.site}/api/series/filter?${params.toString()}`, + ).then(r => + r.json().catch(() => { + throw new Error( + 'There was an error fetching the data from the server. Please try to open it in WebView', + ); + }), + ); + + return (res.data || []).map((r: APINovel) => this.parseNovelFromApi(r)); + } + + async parseNovel(novelPath: string): Promise { + let cleanNovelPath = novelPath; + let apiRes = await fetchApi( + `${this.site}/api/new/v2/series/${novelPath}/chapters`, + {}, + ); + + if (!apiRes.ok) { + const slugMatch = novelPath.match(/^\d+-(.+)$/); + const searchSlug = slugMatch ? slugMatch[1] : novelPath; + apiRes = await fetchApi( + `${this.site}/api/new/v2/series/${searchSlug}/chapters`, + {}, + ); + cleanNovelPath = searchSlug; + + if (!apiRes.ok) { + const words = searchSlug.replace(/-/g, ' ').split(' '); + const SearchStr = words.find(w => w.length > 3) || words[0]; + const searchRes = await fetchApi( + `${this.site}/api/series/filter?page=1&per_page=20&search=${encodeURIComponent(SearchStr)}`, + ).then(r => r.json()); + + if (searchRes.data && searchRes.data.length > 0) { + cleanNovelPath = searchRes.data[0].slug; + apiRes = await fetchApi( + `${this.site}/api/new/v2/series/${cleanNovelPath}/chapters`, + {}, + ); + } + } + + if (!apiRes.ok) { + throw new Error( + 'Novel not found. It may have been removed or its URL changed significantly.', + ); + } + } + + const seriesData: APINovel = await fetchApi( + `${this.site}/api/new/v2/series/${cleanNovelPath}`, + ).then(r => r.json()); + const summaryCheerio = loadCheerio(seriesData.description || ''); + + const novel: Plugin.SourceNovel = { + path: cleanNovelPath, + name: seriesData.title || '', + summary: + summaryCheerio('p').length > 0 + ? summaryCheerio('p') + .map((_, el) => loadCheerio(el).text()) + .get() + .join('\n\n') + : summaryCheerio.text() || '', + author: seriesData.user?.name || seriesData.user?.username || '', + cover: seriesData.cover + ? this.site + '/' + seriesData.cover + : defaultCover, + genres: (seriesData.genres || []).map(g => g.name).join(','), + status: seriesData.status || 'Unknown', + }; + + let chapters = await apiRes.json(); + + if (this.hideLocked) { + chapters = chapters.filter((c: APIChapter) => !c.locked?.price); + } + + novel.chapters = chapters + .map((c: APIChapter) => ({ + name: + (c.locked?.price ? '🔒 ' : '') + + (c.group?.index == null ? '' : 'Vol ' + c.group?.index + ' ') + + 'Chapter ' + + c.number + + (c.title && c.title.trim() != 'Chapter ' + c.number + ? ' - ' + c.title.replace(/^chapter [0-9]+ . /i, '') + : ''), + path: + novelPath + + (c.group?.index == null ? '' : '/' + c.group?.slug) + + '/' + + (c.slug || 'chapter-' + c.number) + + '~~' + + c.id, + releaseTime: c.created_at, + chapterNumber: c.number + (c.group?.index || 0) * 10000, + })) + .sort( + (a: ChapterInfo, b: ChapterInfo) => a.chapterNumber - b.chapterNumber, + ); + return novel; + } + + async parseChapter(chapterPath: string): Promise { + const chapterId = chapterPath.split('~~')[1]; + if (chapterId) { + const url = `${this.site}/api/new/v2/chapters/${chapterId}`; + const res = await fetchApi(url); + const json = await res.json(); + const content = json.content; + + if (content) { + const parsedContent: Chapter = JSON.parse(content); + if (parsedContent.type === 'doc') { + return parsedContent.content + .map(node => { + if (node.type === 'paragraph') { + const innerHtml = + node.content + ?.map(c => { + if (c.type === 'text') { + let text = c.text; + if (c.marks) { + for (const mark of c.marks) { + if (mark.type === 'bold') text = `${text}`; + if (mark.type === 'italic') text = `${text}`; + if (mark.type === 'underline') + text = `${text}`; + if (mark.type === 'strike') + text = `${text}`; + if (mark.type === 'link') + text = `
${text}`; + } + } + return text; + } + if (c.type === 'hardBreak') return '
'; + return ''; + }) + .join('') || ''; + return `

${innerHtml}

`; + } + if (node.type === 'heading') { + const level = node.attrs?.level || 1; + const innerHtml = node.content?.map(c => c.text).join('') || ''; + return `${innerHtml}`; + } + return ''; + }) + .join('\n'); + } + } + } + + // Fallback or old method + const url = `${this.site}/series/${chapterPath.split('~~')[0]}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + let chapterText = loadedCheerio('div.content-area p') + .map((_, el) => `

${loadCheerio(el).html()}

`) + .get() + .join('\n'); + + if (chapterText) { + return chapterText; + } + + // Fallback to SvelteKit JSON if HTML parsing fails or is empty + try { + const jsonUrl = `${this.site}/series/${chapterPath.split('~~')[0]}/__data.json?x-sveltekit-invalidated=001`; + const jsonRes = await fetchApi(jsonUrl); + const json: Nodes = await jsonRes.json(); + + const nodes = json.nodes; + const data = nodes?.find(n => n.type === 'data')?.data; + if (data) { + const contentStr = data.find( + d => typeof d === 'string' && d.includes('{"type":"doc"'), + ); + + if (contentStr) { + const contentJson: Chapter = JSON.parse(contentStr); + if (contentJson.type === 'doc') { + chapterText = contentJson.content + .map(node => { + if (node.type === 'paragraph') { + const innerHtml = + node.content + ?.map(c => { + if (c.type === 'text') { + let text = c.text; + if (c.marks) { + for (const mark of c.marks) { + if (mark.type === 'bold') text = `${text}`; + if (mark.type === 'italic') + text = `${text}`; + } + } + return text; + } + return ''; + }) + .join('') || ''; + return `

${innerHtml}

`; + } + return ''; + }) + .join('\n'); + } + } + } + } catch (e) { + // ignore + } + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + let url = `${this.site}/api/series/filter?page=${pageNo}&per_page=20&search=${encodeURIComponent( + searchTerm, + )}`; + let res = await fetchApi(url).then(r => r.json()); + + if (pageNo === 1 && (!res.data || res.data.length === 0)) { + const words = searchTerm.split(' '); + const fallbackTerm = words.find(w => w.length > 3) || words[0]; + if (fallbackTerm && fallbackTerm !== searchTerm) { + url = `${this.site}/api/series/filter?page=${pageNo}&per_page=20&search=${encodeURIComponent( + fallbackTerm, + )}`; + res = await fetchApi(url).then(r => r.json()); + } + } + + return (res.data || []).map((novel: APINovel) => + this.parseNovelFromApi(novel), + ); + } + + parseNovelFromApi(apiData: APINovel) { + return { + name: apiData.title, + path: apiData.slug, + cover: this.site + '/' + apiData.cover, + summary: apiData.description, + status: apiData.status, + genres: apiData.genres.map(g => g.name).join(','), + }; + } + + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/series/' + path.split('~~')[0]; + + filters = { + status: { + type: FilterTypes.Picker, + label: 'Status', + value: 'any', + options: [ + { label: 'All', value: 'any' }, + { label: 'Ongoing', value: 'ongoing' }, + { + label: 'Completed', + value: 'completed', + }, + ], + }, + sort: { + type: FilterTypes.Picker, + label: 'Sort', + value: 'popular', + options: [ + { label: 'Popular', value: 'popular' }, + { label: 'Latest', value: 'latest' }, + { label: 'Updated', value: 'updated' }, + ], + }, + genres: { + type: FilterTypes.CheckboxGroup, + label: 'Genres', + value: [], + options: [ + { 'label': 'Action', 'value': '1' }, + { 'label': 'Adult', 'value': '2' }, + { + 'label': 'Adventure', + 'value': '3', + }, + { 'label': 'Comedy', 'value': '4' }, + { 'label': 'Drama', 'value': '5' }, + { + 'label': 'Ecchi', + 'value': '6', + }, + { 'label': 'Fantasy', 'value': '7' }, + { 'label': 'Gender Bender', 'value': '8' }, + { + 'label': 'Harem', + 'value': '9', + }, + { 'label': 'Historical', 'value': '10' }, + { 'label': 'Horror', 'value': '11' }, + { + 'label': 'Josei', + 'value': '12', + }, + { 'label': 'Martial Arts', 'value': '13' }, + { 'label': 'Mature', 'value': '14' }, + { + 'label': 'Mecha', + 'value': '15', + }, + { 'label': 'Mystery', 'value': '16' }, + { 'label': 'Psychological', 'value': '17' }, + { + 'label': 'Romance', + 'value': '18', + }, + { 'label': 'School Life', 'value': '19' }, + { 'label': 'Sci-fi', 'value': '20' }, + { + 'label': 'Seinen', + 'value': '21', + }, + { 'label': 'Shoujo', 'value': '22' }, + { 'label': 'Shoujo Ai', 'value': '23' }, + { + 'label': 'Shounen', + 'value': '24', + }, + { 'label': 'Shounen Ai', 'value': '25' }, + { 'label': 'Slice of Life', 'value': '26' }, + { + 'label': 'Smut', + 'value': '27', + }, + { 'label': 'Sports', 'value': '28' }, + { 'label': 'Supernatural', 'value': '29' }, + { + 'label': 'Tragedy', + 'value': '30', + }, + { 'label': 'Wuxia', 'value': '31' }, + { 'label': 'Xianxia', 'value': '32' }, + { + 'label': 'Xuanhuan', + 'value': '33', + }, + { 'label': 'Yaoi', 'value': '34' }, + { 'label': 'Yuri', 'value': '35' }, + ], + }, + } satisfies Filters; +} + +export default new FenrirRealmPlugin(); diff --git a/plugins/english/fictionzone.ts b/plugins/english/fictionzone.ts new file mode 100644 index 000000000..27bbba37c --- /dev/null +++ b/plugins/english/fictionzone.ts @@ -0,0 +1,149 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; + +class FictionZonePlugin implements Plugin.PluginBase { + id = 'fictionzone'; + name = 'Fiction Zone'; + icon = 'src/en/fictionzone/icon.png'; + site = 'https://fictionzone.net'; + version = '1.0.2'; + filters: Filters | undefined = undefined; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + // filters, + }: Plugin.PopularNovelsOptions, + ): Promise { + return await this.getPage( + `/platform/browse?page=${pageNo}&page_size=20&sort_by=${showLatestNovels ? 'created_at' : 'bookmark_count'}&sort_order=desc&include_genres=true`, + ); + } + + // TODO: Fix URLs, current URL leads to 404 + async getData(url: string): Promise> { + return await fetchApi(this.site + '/api/__api_party/fictionzone', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify({ + 'path': url, + 'headers': [ + ['content-type', 'application/json'], + ['x-request-time', new Date().toISOString()], + ], + 'method': 'GET', + }), + }).then(r => r.json()); + } + + async getPage(url: string): Promise { + const data = await this.getData<{ novels: NovelItem[] }>(url); + + return data.data.novels.map(n => ({ + name: n.title, + cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${n.image}.webp`, + path: `novel/${n.slug}`, + })); + } + + async getChapterPage( + id: string, + novelPath: string, + ): Promise { + const data = await this.getData<{ chapters: ChapterItem[] }>( + '/platform/chapter-lists?novel_id=' + id, + ); + + return data.data.chapters.map(n => ({ + name: n.title, + number: n.chapter_number, + date: n.published_date + ? new Date(n.published_date).toISOString() + : undefined, + path: `${novelPath}/${n.chapter_id}|/platform/chapter-content?novel_id=${id}&chapter_id=${n.chapter_id}`, + })); + } + + async parseNovel(novelPath: string): Promise { + const novelSlug = novelPath.replace('novel/', ''); + const data = await this.getData( + `/platform/novel-details?slug=${novelSlug}`, + ); + + return { + path: novelPath, + name: data.data.title, + cover: `https://cdn.fictionzone.net/insecure/rs:fill:165:250/${data.data.image}.webp`, + genres: [ + ...data.data.genres.map(g => g.name), + ...data.data.tags.map(g => g.name), + ].join(','), + status: + data.data.status == 1 + ? NovelStatus.Ongoing + : data.data.status == 0 + ? NovelStatus.Completed + : NovelStatus.Unknown, + author: + data.data.contributors.filter(c => c.role == 'author')[0] + ?.display_name || '', + summary: data.data.synopsis, + chapters: await this.getChapterPage(data.data.id, novelPath), + }; + } + + async parseChapter(chapterPath: string): Promise { + const data = await this.getData<{ content: string }>( + chapterPath.split('|')[1], + ); + return '

' + data.data.content.split('\n').join('

') + '

'; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise { + return await this.getPage( + `/platform/browse?search=${encodeURIComponent(searchTerm)}&page=${pageNo}&page_size=20&search_in_synopsis=true&sort_by=bookmark_count&sort_order=desc&include_genres=true`, + ); + } + + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/' + path.split('|')[0]; +} + +export default new FictionZonePlugin(); + +type Response = { + data: T; +}; + +type NovelItem = { + title: string; + image: string; + slug: string; +}; + +type ChapterItem = { + title: string; + chapter_number: number; + published_date: string; + chapter_id: string; +}; + +type NovelDetails = { + id: string; + title: string; + image: string; + genres: { name: string }[]; + tags: { name: string }[]; + status: number; + contributors: { role: string; display_name: string }[]; + synopsis: string; +}; diff --git a/plugins/english/foxteller.ts b/plugins/english/foxteller.ts new file mode 100644 index 000000000..c257c2be2 --- /dev/null +++ b/plugins/english/foxteller.ts @@ -0,0 +1,331 @@ +import { Plugin } from '@/types/plugin'; +import { Parser } from 'htmlparser2'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi, FetchInit } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; + +class Foxteller implements Plugin.PluginBase { + id = 'foxteller'; + name = 'Foxteller'; + site = 'https://www.foxteller.com'; + version = '1.0.3'; + icon = 'src/en/foxteller/icon.png'; + + async safeFecth(url: string, init?: FetchInit): Promise { + const r = await fetchApi(url, init); + if (!r.ok) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const data = await r.text(); + const title = data.match(/(.*?)<\/title>/)?.[1]?.trim(); + + if ( + title && + (title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...') + ) + throw new Error('Captcha error, please open in webview'); + + return data; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + '/library?sort=' + filters?.order?.value || 'popularity'; + + const body = await this.safeFecth(url); + const novels: Plugin.NovelItem[] = []; + + const div = body.match(/<div class="col-md-6">([\s\S]*?)<\/div>/g) || []; + div.forEach(elements => { + const [, novelUrl, novelName] = + elements.match(/<a href="(.*?)" title="(.*?)">/) || []; + + if (novelName && novelUrl) { + const novelCover = elements.match( + /<img class="img-fluid" src="(.*?)".*>/, + ); + + novels.push({ + name: novelName, + cover: novelCover?.[1] || defaultCover, + path: novelUrl.split('/')[4], + }); + } + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await this.safeFecth(this.resolveUrl(novelPath)); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + genres: '', + summary: '', + status: '', + chapters: [] as Plugin.ChapterItem[], + }; + let isParsingGenres = false; + let isReadingGenre = false; + let isReadingSummary = false; + let isParsingInfo = false; + let isReadingInfo = false; + let isParsingChapterList = false; + let isReadingChapter = false; + let isPaidChapter = false; + const chapters: Plugin.ChapterItem[] = []; + let tempChapter = {} as Plugin.ChapterItem; + + const parser = new Parser({ + onopentag(name, attribs) { + // name and cover + if (!novel.cover && attribs['class'] === 'img-fluid') { + novel.name = attribs['alt']; + novel.cover = attribs['src'] || defaultCover; + } // genres + else if (name === 'div' && attribs['class'] === 'novel-genres') { + isParsingGenres = true; + } else if (isParsingGenres && name === 'li') { + isReadingGenre = true; + } // summary + else if (name === 'div' && attribs['class'] === 'novel-description') { + isReadingSummary = true; + } // status + else if (name === 'div' && attribs['class'] === 'novel-tags') { + isParsingInfo = true; + } else if (isParsingInfo && name === 'li') { + isReadingInfo = true; + } + // chapters + else if (name === 'div' && attribs['class'] === 'col-md-6') { + isParsingChapterList = true; + } else if (isParsingChapterList && name === 'a') { + isReadingChapter = true; + tempChapter.chapterNumber = chapters.length + 1; + tempChapter.path = novelPath + '/' + attribs['href'].split('/')[5]; + } else if ( + isReadingChapter && + name === 'i' && + attribs['class']?.includes('lock') + ) { + isPaidChapter = true; + } + }, + ontext(data) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + novel.genres += data + ', '; + } + } // summary + else if (isReadingSummary) { + novel.summary += data.trim(); + } // status + else if (isParsingInfo) { + if (isReadingInfo) { + const detailName = data.toLowerCase().trim(); + switch (detailName) { + case 'completed': + novel.status = NovelStatus.Completed; + break; + case 'ongoing': + novel.status = NovelStatus.Ongoing; + break; + case 'hiatus': + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + } + // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + tempChapter.name = (tempChapter.name || '') + data; + } + } + }, + onclosetag(name) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + isReadingGenre = false; // stop reading genre + } else { + isParsingGenres = false; // stop parsing genres + novel.genres = novel.genres?.slice(0, -2); // remove trailing comma + } + } // summary + else if (isReadingSummary) { + if (name === 'hr' || name === 'p') { + novel.summary += '\n'; + } else if (name === 'div') { + isReadingSummary = false; + } + } // status + else if (isParsingInfo) { + if (name === 'li') { + isReadingInfo = false; + } else if (name === 'div') { + isParsingInfo = false; + } + } // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + if (name === 'li') { + if (isPaidChapter) { + isPaidChapter = false; + } else { + tempChapter.name = tempChapter.name.trim(); + chapters.push(tempChapter); + } + isReadingChapter = false; + tempChapter = {} as Plugin.ChapterItem; + } + } else if (name === 'ul') { + isParsingChapterList = false; + } + } + }, + }); + + parser.write(body); + parser.end(); + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const res = await this.safeFecth(this.resolveUrl(chapterPath)); + const novelID = chapterPath.split('/')[0]; + const chapterID = res.match(/'chapter_id': '([\d]+)'/)?.[1]; + + if (!chapterID) throw new Error('No chapter found'); + + const { aux } = await fetchApi(this.site + '/aux_dem', { + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json;charset=utf-8', + Referer: this.resolveUrl(chapterPath), + }, + body: JSON.stringify({ 'x1': novelID, 'x2': chapterID }), + }).then(res => res.json()); + + if (aux && typeof aux === 'string') { + const base64 = aux.replace(/%R([a-f])&/g, (match, code) => { + switch (code) { + case 'a': + return 'A'; + case 'c': + return 'B'; + case 'b': + return 'C'; + case 'd': + return 'D'; + case 'f': + return 'E'; + case 'e': + return 'F'; + default: + return match; + } + }); + + return decodeURIComponent(decodeBase64(base64)); + } + + throw new Error('This chapter is closed'); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const body = await this.safeFecth(this.site + '/search', { + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json;charset=utf-8', + Referer: this.site, + }, + body: JSON.stringify({ query: searchTerm }), + }); + const novels: Plugin.NovelItem[] = []; + + const items = body.match(/<a.*>([\s\S]*?)<\/a>/g) || []; + items.forEach(elements => { + const novelUrl = elements.match(/<a href="(.*?)"/)?.[1] || ''; + const path = novelUrl.split('/')[4]; + const name = elements.match( + /<span class="ellipsis-1">(.*?)<\/span>/, + )?.[1]; + + if (name && path) { + const cover = + elements.match(/<img src="(.*?)".*>/)?.[1] || defaultCover; + novels.push({ name, cover, path }); + } + }); + + return novels; + } + + resolveUrl = (path: string) => this.site + '/novel/' + path; + + filters = { + order: { + value: 'popularity', + label: 'Order by', + options: [ + { label: 'Popular Novels', value: 'popularity' }, + { label: 'New Novels', value: 'newest' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Foxteller(); + +const base64Characters = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + +function decodeBase64(encodedString: string) { + const hexcode = []; + let i = 0; + + while (i < encodedString.length) { + const enc1 = base64Characters.indexOf(encodedString.charAt(i++)); + const enc2 = base64Characters.indexOf(encodedString.charAt(i++)); + const enc3 = base64Characters.indexOf(encodedString.charAt(i++)); + const enc4 = base64Characters.indexOf(encodedString.charAt(i++)); + + const chr1 = (enc1 << 2) | (enc2 >> 4); + const chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); + const chr3 = ((enc3 & 3) << 6) | enc4; + + hexcode.push(chr1.toString(16).padStart(2, '0')); + + if (enc3 !== 64) { + hexcode.push(chr2.toString(16).padStart(2, '0')); + } + if (enc4 !== 64) { + hexcode.push(chr3.toString(16).padStart(2, '0')); + } + } + + return '%' + hexcode.join('%'); +} diff --git a/plugins/english/genesis.ts b/plugins/english/genesis.ts new file mode 100644 index 000000000..e6644b3b1 --- /dev/null +++ b/plugins/english/genesis.ts @@ -0,0 +1,312 @@ +import { load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { storage } from '@libs/storage'; +import { defaultCover } from '@libs/defaultCover'; + +class Genesis implements Plugin.PluginBase { + id = 'genesistudio'; + name = 'Genesis'; + icon = 'src/en/genesis/icon.png'; + customCSS = 'src/en/genesis/customCSS.css'; + site = 'https://genesistudio.com'; + api = 'https://api.genesistudio.com'; + version = '2.0.1'; + + hideLocked = storage.get('hideLocked'); + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + imageRequestInit?: Plugin.ImageRequestInit = { + headers: { + 'referrer': this.site, + }, + }; + + async parseNovelJSON(): Promise<Plugin.SourceNovel[]> { + // Thought about caching this, + // but not sure what would happen if a new novel were to be + // added to the library, so, fetch everytime it is + // + // fields param literally gives you the JSON you want + // maybe TODO: add filtering + const params = new URLSearchParams({ + status: 'published', + fields: '["id","novel_title","cover","abbreviation"]', + limit: '-1', + }); + const link = `${this.site}/api/directus/novels?${params.toString()}`; + const json: NovelJSON[] = await fetchApi(link).then(r => r.json()); + return json.map(novel => ({ + name: novel.novel_title, + path: `/novels/${novel.abbreviation}`.trim(), + cover: `${this.api}/storage/v1/object/public/directus/${novel.cover}.png`, + })); + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + // There is only one page of results, and no known page function, so do not try + if (pageNo !== 1) return []; + return this.parseNovelJSON(); + } + + async getCoverUrl(coverId: string): Promise<string> { + // genesis doesn't actually use jpegs but just in case + const ext = await fetchApi(`${this.site}/api/directus-file/${coverId}`) + .then(res => res.json()) + .then(data => + data.type ? data.type.split('/')[1].replace('jpeg', 'jpg') : 'png', + ) + .catch(() => 'png'); + + return `${this.api}/storage/v1/object/public/directus/${coverId}.${ext}`; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const abbreviation = novelPath.replace('/novels/', ''); + const url = `${this.site}/api/directus/novels/by-abbreviation/${abbreviation}`; + + // Fetch the novel's data in JSON format + const raw = await fetchApi(url); + const json: NovelJSON = await raw.json(); + + const novel: Plugin.SourceNovel = { + name: json.novel_title, + path: novelPath, + summary: json.synopsis, + author: json.author, + cover: json.cover ? await this.getCoverUrl(json.cover) : defaultCover, + genres: json.genres + ?.map(g => g.genres_id?.label) + .filter(l => l) + .join(','), + }; + + const map: Record<string, string> = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + unknown: NovelStatus.Unknown, + }; + novel.status = + map[json.serialization?.toLowerCase() || ''] ?? NovelStatus.Unknown; + + // Parse the chapters if available and assign them to the novel object + novel.chapters = await this.extractChapters(json.id); + + return novel; + } + + // Helper function to extract and format chapters + async extractChapters(id: string): Promise<Plugin.ChapterItem[]> { + const url = `${this.site}/api/novels-chapter/${id}`; + + // Fetch the chapter data in JSON format + const raw = await fetchApi(url); + const json: ChapterJSON = await raw.json(); + + // Format each chapter and add only valid ones + const chapters = json.data.chapters + .map(index => { + const title = index.chapter_title; + const chapterPath = `/viewer/${index.id}`; + const isLocked = !index.isUnlocked; + if (this.hideLocked && isLocked) return null; + const chapterName = isLocked ? '🔒 ' + title : title; + const chapterNum = index.chapter_number; + + if (!chapterPath) return null; + + return { + name: chapterName, + path: chapterPath, + chapterNumber: Number(chapterNum), + }; + }) + .filter(chapter => chapter !== null); + + return chapters; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = `${this.site}${chapterPath}`; + const id = chapterPath.replace('/viewer/', ''); + + // Fetch the novel's data in JSON format + const raw = await fetchApi(url); + const $ = load(await raw.text()); + let external_api; + let apikey; + + const URLs: string[] = []; + let code; + + $('head script[src]').each((_, el) => { + const src = $(el).attr('src')!; + if (!URLs.includes(src)) { + URLs.push(src); + } + }); + + for (const src of URLs) { + const script = await fetchApi(`${this.site}${src}`); + const raw = await script.text(); + if (raw.includes('sb_publishable')) { + code = raw; + break; + } + } + if (!code) { + throw new Error('Failed to find API Key'); + } + // Find right segment of code + let arr = code.split(';'); + for (const seg of arr) { + if (seg.includes('sb_publishable')) { + code = seg; + break; + } + } + arr = code.split('"'); + for (const seg of arr) { + if (seg.includes('https')) { + external_api = seg; + continue; + } + if (seg.includes('sb_publishable')) { + apikey = seg; + continue; + } + } + + const path = `${external_api}/rest/v1/chapters`; + const search = new URLSearchParams({ + select: 'id,chapter_title,chapter_number,chapter_content,status,novel', + id: `eq.${id}`, + status: 'eq.released', + }); + + const chQuery = await fetchApi(`${path}?${search}`, { + method: 'GET', + headers: { + // Cookie: 'csrftoken=' + csrftoken, + Referer: this.site, + 'apikey': apikey, + 'x-client-info': 'supabase-ssr/0.7.0 createBrowserClient', + }, + }); + const json = await chQuery.json(); + const ch = json[0].chapter_content.replaceAll('\n', '<br/>'); + return ch; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.SourceNovel[]> { + if (pageNo !== 1) return []; + // Since only 26 novels, fetch all the novels + // then filter out the novels which match the criteria + const novels = await this.parseNovelJSON(); + const query = this.normalize(searchTerm); + + return novels.filter(novel => this.normalize(novel.name).includes(query)); + } + + // grabbed from Witch Cult Translations + private normalize(str: string) { + return str.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + + // due to the low amount of novels, using filters kinda overkill + // unless we apply filters to cached results + filters = { + sort: { + label: 'Sort Results By', + value: 'Date', + options: [ + { label: 'Date', value: 'Date' }, + { label: 'Views', value: 'Views' }, + ], + type: FilterTypes.Picker, + }, + // storyStatus: { + // label: 'Status', + // value: 'All', + // options: [ + // { label: 'All', value: 'All' }, + // { label: 'Ongoing', value: 'Ongoing' }, + // { label: 'Completed', value: 'Completed' }, + // ], + // type: FilterTypes.Picker, + // }, + genres: { + label: 'Genres', + value: [], + options: [ + { 'label': 'Academy', 'value': '21' }, + { 'label': 'Action', 'value': '1' }, + { 'label': 'Adventure', 'value': '15' }, + { 'label': 'Calm Protagonist', 'value': '22' }, + { 'label': 'Comedy', 'value': '2' }, + { 'label': 'Cultivation', 'value': '25' }, + { 'label': 'Drama', 'value': '3' }, + { 'label': 'Fantasy', 'value': '5' }, + { 'label': 'Harem', 'value': '11' }, + { 'label': 'Idol', 'value': '20' }, + { 'label': 'Martial Arts', 'value': '6' }, + { 'label': 'Modern', 'value': '4' }, + { 'label': 'Modern Fantasy', 'value': '27' }, + { 'label': 'Mystery', 'value': '8' }, + { 'label': 'Psychological', 'value': '10' }, + { 'label': 'Romance', 'value': '9' }, + { 'label': 'School Life', 'value': '13' }, + { 'label': 'Sci-fi', 'value': '24' }, + { 'label': 'Slice of Life', 'value': '7' }, + { 'label': 'Supernatural', 'value': '14' }, + { 'label': 'Tragedy', 'value': '12' }, + { 'label': 'Transmigration', 'value': '23' }, + { 'label': 'Yandere', 'value': '26' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new Genesis(); + +type NovelJSON = { + id: string; + novel_title: string; + abbreviation: string; + cover: string; + synopsis?: string; + author?: string; + serialization?: string; + genres?: { + genres_id?: { + id?: number; + label?: string; + }; + }[]; +}; + +type ChapterJSON = { + data: { + chapters: { + id: string; + chapter_number: number; + chapter_title: string; + isUnlocked: boolean; + }[]; + }; +}; diff --git a/plugins/english/indraTranslations.ts b/plugins/english/indraTranslations.ts new file mode 100644 index 000000000..e8aed1002 --- /dev/null +++ b/plugins/english/indraTranslations.ts @@ -0,0 +1,297 @@ +import { load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; + +class IndraTranslations implements Plugin.PluginBase { + id = 'indratranslations'; + name = 'Indra Translations'; + site = 'https://indratranslations.com'; + version = '1.2.1'; + icon = 'src/en/indratranslations/icon.png'; + // customCSS = 'src/en/indratranslations/customCSS.css'; + // (optional) Add these files to the repo and uncomment the lines above if you want an icon/custom CSS. + + // Browser-like headers (important for Cloudflare-y sites) + private headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36', + Referer: this.site, + Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Cache-Control': 'no-cache', + Pragma: 'no-cache', + }; + + private async fetchHtml(url: string): Promise<string> { + const res = await fetchApi(url, { headers: this.headers }); + return await res.text(); + } + + private absolute(url?: string): string | undefined { + if (!url) return undefined; + const u = String(url).trim(); + if (!u) return undefined; + if (u.startsWith('http')) return u; + if (u.startsWith('//')) return 'https:' + u; + if (u.startsWith('/')) return this.site + u; + return this.site + '/' + u; + } + + private clean(text: unknown): string { + return String(text ?? '') + .replace(/\s+/g, ' ') + .trim(); + } + + private chapterNum(name: string): number { + const m = String(name).match(/(\d+(\.\d+)?)/); + return m ? Number(m[1]) : 0; + } + + /** + * Indra can render results in different templates. + * This tries multiple layouts and returns a single unified list. + */ + private parseNovelCards($: ReturnType<typeof load>) { + const out: { name: string; path: string; cover?: string }[] = []; + const seen = new Set<string>(); + + const push = (name?: string, path?: string, cover?: string) => { + const cleanName = this.clean(name); + const cleanPath = String(path || '') + .replace(this.site, '') + .trim(); + if (!cleanName || !cleanPath) return; + if (!cleanPath.includes('/series/')) return; + + // Normalize trailing slash for consistency + const normalized = cleanPath.endsWith('/') ? cleanPath : cleanPath + '/'; + if (seen.has(normalized)) return; + + seen.add(normalized); + out.push({ + name: cleanName, + path: normalized, + cover: cover ? this.absolute(cover) : undefined, + }); + }; + + // -------- Layout A (Madara-style): .page-item-detail ---------- + $('.page-item-detail').each((_, el) => { + const a = $(el).find('a[href*="/series/"]').first(); + const href = a.attr('href') || ''; + const title = + this.clean(a.attr('title')) || + this.clean($(el).find('h3 a').text()) || + this.clean($(el).find('.post-title a').text()); + + const img = + $(el).find('img').attr('data-src') || + $(el).find('img').attr('data-lazy-src') || + $(el).find('img').attr('src'); + + if (href) push(title, href, img || undefined); + }); + + // -------- Layout B (common search tabs): .c-tabs-item__content ---------- + $('.c-tabs-item__content').each((_, el) => { + const a = + $(el).find('a[href*="/series/"]').first() || + $(el).find('.tab-thumb a[href*="/series/"]').first(); + + const href = a.attr?.('href') || ''; + const title = + this.clean($(el).find('.post-title a').text()) || + this.clean($(el).find('.tab-summary .post-title a').text()) || + this.clean(a.attr?.('title')) || + this.clean(a.text?.()); + + const img = + $(el).find('img').attr('data-src') || + $(el).find('img').attr('data-lazy-src') || + $(el).find('img').attr('src'); + + if (href) push(title, href, img || undefined); + }); + + // -------- Layout C (sometimes search results are in .row or .col wrappers) ---------- + $('.row').each((_, el) => { + const a = $(el).find('a[href*="/series/"]').first(); + const href = a.attr('href') || ''; + if (!href) return; + + const title = + this.clean($(el).find('h3 a').text()) || + this.clean($(el).find('.post-title a').text()) || + this.clean(a.attr('title')) || + this.clean(a.text()); + + const img = + $(el).find('img').attr('data-src') || + $(el).find('img').attr('data-lazy-src') || + $(el).find('img').attr('src'); + + push(title, href, img || undefined); + }); + + // -------- Layout D (fallback: any anchor to /series/) ---------- + // If everything else fails but links exist, still return something. + if (out.length === 0) { + $('a[href*="/series/"]').each((_, el) => { + const a = $(el); + const href = a.attr('href') || ''; + if (!href) return; + + const title = + this.clean(a.attr('title')) || this.clean(a.text()) || 'Unknown'; + + // Try to find an image near the link + const img = + a.find('img').attr('data-src') || + a.find('img').attr('data-lazy-src') || + a.find('img').attr('src') || + a.closest('*').find('img').first().attr('data-src') || + a.closest('*').find('img').first().attr('data-lazy-src') || + a.closest('*').find('img').first().attr('src'); + + push(title, href, img || undefined); + }); + } + + return out; + } + + async popularNovels(pageNo: number) { + if (pageNo !== 1) return []; + const html = await this.fetchHtml(`${this.site}/series/`); + const $ = load(html); + const parsed = this.parseNovelCards($); + + return parsed.map(n => ({ + name: n.name, + path: n.path, + cover: n.cover, + })); + } + + async searchNovels(searchTerm: string, pageNo: number) { + if (pageNo !== 1) return []; + const url = `${this.site}/?s=${encodeURIComponent(searchTerm)}&post_type=wp-manga`; + const html = await this.fetchHtml(url); + const $ = load(html); + return this.parseNovelCards($); + } + + async parseNovel(novelPath: string) { + const url = novelPath.startsWith('http') + ? novelPath + : this.site + novelPath; + const html = await this.fetchHtml(url); + const $ = load(html); + + const title = + this.clean($('h1.entry-title').text()) || + this.clean($('h1').first().text()) || + 'Unknown'; + + const cover = this.absolute( + $('.summary_image img').attr('data-src') || + $('.summary_image img').attr('data-lazy-src') || + $('.summary_image img').attr('src'), + ); + + const summary = + this.clean($('.summary__content').text()) || + this.clean($('.description-summary').text()) || + undefined; + + let statusText = ''; + $('.post-content_item').each((_, el) => { + const label = this.clean( + $(el).find('.summary-heading').text(), + ).toLowerCase(); + if (label.includes('status')) { + statusText = this.clean($(el).find('.summary-content').text()); + } + }); + + const chapters: { name: string; path: string; chapterNumber?: number }[] = + []; + + $('li.wp-manga-chapter a').each((_, el) => { + const href = $(el).attr('href'); + if (!href) return; + const name = this.clean($(el).text()); + chapters.push({ + name, + path: href.replace(this.site, ''), + chapterNumber: this.chapterNum(name), + }); + }); + + if (chapters.length === 0) { + $('.wp-manga-chapter a').each((_, el) => { + const href = $(el).attr('href'); + if (!href) return; + const name = this.clean($(el).text()); + chapters.push({ + name, + path: href.replace(this.site, ''), + chapterNumber: this.chapterNum(name), + }); + }); + } + + chapters.sort((a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0)); + + const statusLower = String(statusText).toLowerCase(); + const status = + statusLower.includes('complete') || statusLower.includes('completed') + ? NovelStatus.Completed + : NovelStatus.Ongoing; + + return { + name: title, + path: novelPath.endsWith('/') ? novelPath : novelPath + '/', + cover, + summary, + status, + chapters, + }; + } + + async parseChapter(chapterPath: string) { + const url = chapterPath.startsWith('http') + ? chapterPath + : this.site + chapterPath; + const html = await this.fetchHtml(url); + const $ = load(html); + + const content = $('.reading-content').first().length + ? $('.reading-content').first() + : $('.text-left').first().length + ? $('.text-left').first() + : $('.entry-content').first(); + + if (!content.length) { + return `\nUnable to load chapter content.\n\n`; + } + + content.find('script, style, ins, iframe, noscript').remove(); + + return content.html() ?? ''; + } + + filters: Filters = { + sort: { + label: 'Sort', + value: 'Latest', + options: [{ label: 'Latest', value: 'Latest' }], + type: FilterTypes.Picker, + }, + }; +} + +export default new IndraTranslations(); diff --git a/plugins/english/inkitt.ts b/plugins/english/inkitt.ts new file mode 100644 index 000000000..585a3df5a --- /dev/null +++ b/plugins/english/inkitt.ts @@ -0,0 +1,190 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; + +class InkittPlugin implements Plugin.PluginBase { + id = 'inkitt'; + name = 'Inkitt'; + icon = 'src/en/inkitt/icon.png'; + site = 'https://www.inkitt.com'; + version = '1.0.1'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { + // showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let req; + if (filters?.genres?.value) { + req = await fetchApi( + this.site + + `/genre/${filters.genres.value}/${pageNo}?period=alltime&sort=popular`, + ); + } else { + req = await fetchApi( + this.site + `/trending_stories?page=${pageNo}&period=alltime`, + ); + } + let data; + try { + data = await req.json(); + } catch (e) { + throw new Error('Failed to load novels, try opening in webview.'); + } + + return data.stories.map((novel: InkittNovel) => { + return { + name: novel.title, + path: this.getPath(novel), + cover: + novel.vertical_cover.url || + novel.vertical_cover.iphone || + novel.cover.url, + }; + }); + } + + getPath(novel: InkittNovel) { + return (novel.category_one || novel.genres[0]) + '/' + novel.id; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const req = await fetchApi(this.site + `/stories/${novelPath}`); + const text = await req.text(); + const loadedCheerio = loadCheerio(text); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.story-title').text(), + }; + + novel.author = loadedCheerio('dl > dd > a.author-link').text(); + + novel.genres = loadedCheerio('dd.genres > a') + .map((_, el) => loadedCheerio(el).text()) + .toArray() + .join(', '); + const status = loadedCheerio('div.dlc > dl:has(dt:contains("Status")) > dd') + .text() + .trim(); + if (status === 'Complete') novel.status = NovelStatus.Completed; + if (status === 'Ongoing') novel.status = NovelStatus.Ongoing; + + const apiReq = await fetchApi( + this.site + `/api/stories/${novelPath.split('/')[1]}`, + ); + const apiData = (await apiReq.json()) as InkittApi; + novel.cover = apiData.vertical_cover.url; + + novel.summary = loadedCheerio('p.story-summary').text(); + + novel.chapters = apiData.chapters.map((c: InkittChapter) => { + return { + name: c.name, + path: novelPath + '/chapters/' + c.chapter_number, + chapterNumber: c.chapter_number, + }; + }); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const req = await fetchApi(this.site + '/stories/' + chapterPath); + const text = await req.text(); + const loadedCheerio = loadCheerio(text); + + return loadedCheerio('div#chapterText').html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const req = await fetchApi( + this.site + + `/api/2/search/title?q=${encodeURIComponent(searchTerm)}&page=${pageNo}`, + ); + let data; + try { + data = await req.json(); + } catch (e) { + throw new Error('Failed to search novels, try opening in webview.'); + } + + return data.stories.map((novel: InkittNovel) => { + return { + name: novel.title, + path: this.getPath(novel), + cover: + novel.vertical_cover.url || + novel.vertical_cover.iphone || + novel.cover.url, + }; + }); + } + + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/stories/' + path; + + filters = { + genres: { + type: FilterTypes.Picker, + label: 'Genre', + value: '', + options: [ + { 'label': 'Sci-Fi', 'value': 'scifi' }, + { 'label': 'Fantasy', 'value': 'fantasy' }, + { 'label': 'Adventure', 'value': 'adventure' }, + { 'label': 'Mystery', 'value': 'mystery' }, + { 'label': 'Action', 'value': 'action' }, + { 'label': 'Horror', 'value': 'horror' }, + { 'label': 'Humor', 'value': 'humor' }, + { 'label': 'Erotica', 'value': 'erotica' }, + { 'label': 'Poetry', 'value': 'poetry' }, + { 'label': 'Other', 'value': 'other' }, + { 'label': 'Thriller', 'value': 'thriller' }, + { 'label': 'Romance', 'value': 'romance' }, + { 'label': 'Children', 'value': 'children' }, + { 'label': 'Drama', 'value': 'drama' }, + ], + }, + } satisfies Filters; +} + +export default new InkittPlugin(); + +// Typings inferred from usage, no actual investigation done +// TODO: change layout +type InkittNovel = { + id: number; + title: string; + category_one?: string; + genres: string[]; + cover: { + url: string; + }; + vertical_cover: { + url: string; + iphone: string; + }; +}; + +type InkittChapter = { + name: string; + chapter_number: number; +}; + +type InkittApi = { + vertical_cover: { + url: string; + }; + chapters: InkittChapter[]; +}; diff --git a/plugins/english/inoveltranslation.ts b/plugins/english/inoveltranslation.ts new file mode 100644 index 000000000..28578b482 --- /dev/null +++ b/plugins/english/inoveltranslation.ts @@ -0,0 +1,406 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import { storage } from '@libs/storage'; + +class INovelTranslation implements Plugin.PluginBase { + id = 'inoveltranslation'; + name = 'iNovelTranslation'; + icon = 'src/en/inoveltranslation/icon.png'; + site = 'https://inoveltranslation.com'; + version = '1.0.2'; + filters: Filters | undefined = undefined; + + pluginSettings = { + hideLocked: { + value: false, + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + private readonly HEADERS = { + 'Accept': + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'Referer': 'https://inoveltranslation.com/', + 'Sec-Fetch-Dest': 'empty', + 'Sec-Fetch-Mode': 'cors', + 'Sec-Fetch-Site': 'same-origin', + }; + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/api/novels?limit=50&page=${pageNo}`; + const result: ApiResponse<NovelData> = await fetchApi(url, { + headers: this.HEADERS, + }).then(r => r.json()); + + const novels: Plugin.NovelItem[] = []; + + if (result.docs) { + result.docs.forEach(doc => { + novels.push({ + name: doc.title, + path: `/novels/${doc.id}`, + cover: doc.cover?.url ? this.site + doc.cover.url : defaultCover, + }); + }); + } + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const id = novelPath.split('/').pop(); + const novelUrl = `${this.site}/api/novels/${id}?depth=1`; + const novelData: NovelData = await fetchApi(novelUrl, { + headers: this.HEADERS, + }).then(r => r.json()); + + const chaptersUrl = `${this.site}/api/chapters?where[novel][equals]=${id}&limit=999&depth=0`; + const chaptersData: ApiResponse<ChapterData> = await fetchApi(chaptersUrl, { + headers: this.HEADERS, + }).then(r => r.json()); + + const status = + novelData.publication === 'completed' + ? NovelStatus.Completed + : NovelStatus.Ongoing; + + const genres = novelData.tags + ? novelData.tags.map(tag => tag.name).join(', ') + : ''; + + let summary = ''; + if (novelData.sypnosis && novelData.sypnosis.root) { + summary = this.lexicalToText(novelData.sypnosis.root); + } + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: novelData.title || 'Untitled', + cover: novelData.cover?.url + ? this.site + novelData.cover.url + : defaultCover, + summary: summary, + author: novelData.author?.name || 'Unknown', + genres: genres, + status: status, + }; + + const chapters: Plugin.ChapterItem[] = []; + const hideLocked = storage.get('hideLocked'); + + if (chaptersData.docs) { + chaptersData.docs.forEach(doc => { + const isLocked = doc.tier !== null; + if (isLocked && hideLocked) { + return; + } + + const title = doc.title ? ` - ${doc.title}` : ''; + const lockIcon = isLocked ? ' 🔒' : ''; + + chapters.push({ + name: `Ch. ${doc.chapter}${lockIcon}${title}`, + path: `/chapters/${doc.id}`, + releaseTime: doc.updatedAt, + chapterNumber: doc.chapter, + }); + }); + } + + novel.chapters = chapters.sort( + (a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0), + ); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + await new Promise(res => setTimeout(res, 1500)); + + const rscHeader = { ...this.HEADERS, rsc: '1' }; + + let response; + try { + response = await fetchApi(this.site + chapterPath, { + headers: rscHeader, + }); + } catch (e) { + throw new Error(`Network error: ${(e as Error).message}`); + } + + if (response.status !== 200) { + throw new Error( + `Cloudflare challenge or server error (Status: ${response.status}). Please open in WebView to verify.`, + ); + } + + const rscText = await response.text(); + + if (!rscText || rscText.trim() === '') { + throw new Error('Server returned empty data.'); + } + + if ( + rscText.includes('cf-browser-verification') || + rscText.includes('cf-challenge') || + rscText.includes('cloudflare-static') || + rscText.includes('Just a moment...') + ) { + if (!rscText.includes('root') && !rscText.includes('paragraph')) { + throw new Error( + 'Cloudflare Challenge detected. Please open this novel in WebView to solve the challenge.', + ); + } + } + + // ========================================== + // 2. ROBUST LEXICAL EXTRACTION ALGORITHM + // ========================================== + + // Use a more reliable signature: the start of the root Lexical object + const signatures = [ + '"root":{"type":"root"', + '\\"root\\":{\\"type\\":\\"root\\"', + '"children":[{"type":"paragraph"', + '\\"children\\":[{\\"type\\":\\"paragraph\\"', + ]; + + let sigIndex = -1; + for (const sig of signatures) { + sigIndex = rscText.indexOf(sig); + if (sigIndex !== -1) break; + } + + if (sigIndex !== -1) { + // Backtrack to find the opening brace { of the Lexical Object + let startIndex = rscText.lastIndexOf('{', sigIndex); + + // Check for "content" or "root" before to find the start of the relevant object + const contextKeys = [ + '"content"', + '\\"content\\"', + '"root"', + '\\"root\\"', + ]; + for (const key of contextKeys) { + const keyIndex = rscText.lastIndexOf(key, sigIndex); + if (keyIndex !== -1 && keyIndex > startIndex - 50) { + startIndex = rscText.lastIndexOf('{', keyIndex); + break; + } + } + + if (startIndex !== -1) { + let braces = 0; + let inString = false; + let escape = false; + let jsonStr = ''; + + // Perform brace balancing on the raw stream to preserve escaping + for (let i = startIndex; i < rscText.length; i++) { + const char = rscText[i]; + if (escape) { + escape = false; + continue; + } + if (char === '\\') { + escape = true; + continue; + } + if (char === '"') { + inString = !inString; + continue; + } + + if (!inString) { + if (char === '{') braces++; + else if (char === '}') braces--; + } + + if (braces === 0 && i > startIndex) { + jsonStr = rscText.substring(startIndex, i + 1); + break; + } + } + + if (jsonStr) { + try { + // eslint-disable-next-line no-control-regex + const safeJson = jsonStr.replace(/[\x00-\x1F\x7F-\x9F]/g, ''); + let parsedData; + try { + parsedData = JSON.parse(safeJson); + } catch { + // If fails, it might be escaped, so clean it up and try again + const cleanJson = jsonStr + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\') + // eslint-disable-next-line no-control-regex + .replace(/[\x00-\x1F\x7F-\x9F]/g, ''); + parsedData = JSON.parse(cleanJson); + } + + const lexicalRoot = + parsedData.root || parsedData.content?.root || parsedData; + if (lexicalRoot && lexicalRoot.children) { + return this.lexicalToHtml(lexicalRoot); + } + } catch (e: unknown) { + // Fallback to regex text extraction if JSON parsing fails + let fallbackHtml = ''; + const textMatches = jsonStr.match( + /\\?"text\\?"\s*:\s*\\?"(.*?)\\?"/g, + ); + if (textMatches && textMatches.length > 0) { + textMatches.forEach(m => { + let text = m.match(/: ?"?(.*?)"?$/)?.[1] || ''; + text = text.replace(/\\"/g, '"').replace(/\\\\/g, '\\'); + if (text.trim() && text !== ' ' && !text.startsWith('Ch. ')) { + fallbackHtml += `<p>${text}</p>`; + } + }); + if (fallbackHtml) return fallbackHtml; + } + } + } + } + } + + // ========================================== + // 3. HTML SCAVENGER FALLBACK + // ========================================== + // If RSC extraction failed, try fetching the standard HTML page + try { + const htmlResponse = await fetchApi(this.site + chapterPath, { + headers: this.HEADERS, + }); + const htmlText = await htmlResponse.text(); + const $ = loadCheerio(htmlText); + const htmlContent = $( + 'main > section[data-sentry-component="RichText"]', + ).html(); + if (htmlContent) return htmlContent; + } catch (e) { + // Ignore fallback errors and throw the final error below + } + + throw new Error( + 'Story content not found. The page structure might have changed. Please try opening in WebView to verify.', + ); + } + + private lexicalToHtml(node: LexicalNode): string { + let html = ''; + if (node.children) { + for (const child of node.children) { + if (child.type === 'paragraph') { + html += `<p>${this.lexicalToHtml(child)}</p>`; + } else if (child.type === 'text') { + let text = child.text || ''; + if (child.format && child.format & 1) text = `<b>${text}</b>`; + if (child.format && child.format & 2) text = `<i>${text}</i>`; + html += text; + } else if (child.type === 'list') { + const tag = child.listType === 'number' ? 'ol' : 'ul'; + html += `<${tag}>${this.lexicalToHtml(child)}</${tag}>`; + } else if (child.type === 'listitem') { + html += `<li>${this.lexicalToHtml(child)}</li>`; + } else if (child.type === 'heading') { + const tag = child.tag || 'h3'; + html += `<${tag}>${this.lexicalToHtml(child)}</${tag}>`; + } else { + html += this.lexicalToHtml(child); + } + } + } + return html; + } + + private lexicalToText(node: LexicalNode): string { + let textOut = ''; + if (node.children) { + for (const child of node.children) { + if (child.type === 'paragraph') { + textOut += this.lexicalToText(child) + '\n\n'; + } else if (child.type === 'text') { + textOut += child.text || ''; + } else if (child.type === 'listitem') { + textOut += '• ' + this.lexicalToText(child) + '\n'; + } else { + textOut += this.lexicalToText(child); + } + } + } + return textOut; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/api/novels?where[title][contains]=${encodeURIComponent( + searchTerm, + )}&limit=50&page=${pageNo}`; + const result: ApiResponse<NovelData> = await fetchApi(url, { + headers: this.HEADERS, + }).then(r => r.json()); + + const novels: Plugin.NovelItem[] = []; + + if (result.docs) { + result.docs.forEach(doc => { + novels.push({ + name: doc.title, + path: `/novels/${doc.id}`, + cover: doc.cover?.url ? this.site + doc.cover.url : defaultCover, + }); + }); + } + + return novels; + } +} + +export default new INovelTranslation(); + +type LexicalNode = { + type: string; + text?: string; + children?: LexicalNode[]; + format?: number; + listType?: string; + tag?: string; +}; + +type NovelData = { + id: string; + title: string; + cover?: { + url: string; + }; + author?: { + name: string; + }; + publication?: string; + tags?: { name: string }[]; + sypnosis?: { + root: LexicalNode; + }; +}; + +type ChapterData = { + id: string; + title?: string; + chapter: number; + tier: string | null; + updatedAt: string; +}; + +type ApiResponse<T> = { + docs: T[]; +}; diff --git a/plugins/english/leafstudio.ts b/plugins/english/leafstudio.ts new file mode 100644 index 000000000..ddfaf45d0 --- /dev/null +++ b/plugins/english/leafstudio.ts @@ -0,0 +1,125 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; + +class LeafStudio implements Plugin.PluginBase { + id = 'LeafStudio'; + name = 'LeafStudio'; + icon = 'src/en/leafstudio/icon.png'; + site = 'https://leafstudio.site/'; + version = '1.0.0'; + + filters: Filters | undefined = undefined; + + parseNovelsList(cheerio: CheerioAPI): Plugin.NovelItem[] { + return cheerio('a.novel-item') + .map((i, el) => { + const elc = parseHTML(el); + return { + name: elc('p.novel-item-title').text().trim(), + path: cheerio(el).attr('href')!.replace(this.site, ''), + cover: elc('img.novel-item-Cover').attr('src'), + }; + }) + .toArray(); + } + + async popularNovels( + page: number, + // { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site + 'novels'; + if (page > 1) { + link += '/page/' + page; + } + + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovelsList(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.title').text().trim() || '', + cover: loadedCheerio('img#novel_cover').attr('src') || defaultCover, + summary: loadedCheerio('div.desc_div > p') + .map((i, el) => parseHTML(el).text()) + .toArray() + .join('\n\n'), + chapters: [], + author: '', + genres: loadedCheerio('div#tags_div > a.novel_genre') + .map((i, el) => parseHTML(el).text().trim()) + .toArray() + .join(', '), + }; + const status = loadedCheerio('a#novel_status').text().trim(); + if (status == 'Active') { + novel.status = 'Ongoing'; + } else { + novel.status = status; + } + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('a.free_chap.chap').each((i, el) => { + const path = + loadedCheerio(el).attr('href')?.trim()?.replace(this.site, '') || ''; + const name = loadedCheerio(el).text(); + + chapter.push({ + name, + path, + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + return loadedCheerio('article > p.chapter_content') + .map((i, el) => parseHTML(el).html()) + .toArray() + .join('<br>'); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + let link = this.site + 'novels'; + if (page > 1) { + link += '/page/' + page; + } + + link += + '?search=' + + encodeURIComponent(searchTerm) + + '&type=&language=&status=&sort='; + + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovelsList(loadedCheerio); + } +} + +export default new LeafStudio(); diff --git a/plugins/english/lightnoveltranslation.ts b/plugins/english/lightnoveltranslation.ts new file mode 100644 index 000000000..347a6d009 --- /dev/null +++ b/plugins/english/lightnoveltranslation.ts @@ -0,0 +1,208 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +// import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +// import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +// import { storage, localStorage, sessionStorage } from '@libs/storage'; +// import { encode, decode } from 'urlencode'; +// import dayjs from 'dayjs'; +// import { Parser } from 'htmlparser2'; + +class LNTPlugin implements Plugin.PluginBase { + id = 'lightnoveltranslations'; + name = 'Light Novel Translations'; + icon = 'src/en/lightnoveltranslations/icon.png'; + site = 'https://lightnovelstranslations.com/'; + version = '1.0.0'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + // filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site + 'read/'; + link += `page/${pageNo}`; + link += `?sortby=${showLatestNovels ? 'most-recent' : 'most-liked'}`; + + const body = await fetchApi(link); + const html = await body.text(); + + const loadedCheerio = loadCheerio(html); + + const baseUrl = this.site; + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div.read_list-story-item').each((i, el) => { + const tempNovel = {} as Plugin.NovelItem; + const img = loadedCheerio(el) + .find('.item_thumb') + .find('img') + .first() + .attr('src'); + let path = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('href'); + path = path ? path.slice(baseUrl.length) : ''; + const title = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('title'); + tempNovel.name = title ? title : ''; + tempNovel.path = path; + tempNovel.cover = img; + novels.push(tempNovel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + const body = await fetchApi(url); + const html = await body.text().then(r => r.replace(/>\s+</g, '><')); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: '', + totalPages: 1, + summary: '', + author: '', + status: '', + chapters: [], + }; + + const loadedCheerio = loadCheerio(html); + novel.cover = loadedCheerio('div.novel-image').find('img').attr('src'); + novel.status = loadedCheerio('div.novel_status').text().trim(); + switch (novel.status) { + case 'Ongoing': + novel.status = NovelStatus.Ongoing; + break; + case 'Hiatus': + novel.status = NovelStatus.OnHiatus; + break; + case 'Completed': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + } + novel.name = loadedCheerio('div.novel_title') + .find('h3') + .first() + .text() + .trim(); + novel.author = loadedCheerio('div.novel_detail_info') + .find('li') + .filter(function () { + return loadedCheerio(this).text().includes('Author'); + }) + .first() + .text() + .trim(); + + const body2 = await fetchApi(url.replace('?tab=table_contents', '')); + const html2 = await body2.text().then(r => r.replace(/>\s+</g, '><')); + const loadedCheerio2 = loadCheerio(html2); + + novel.summary = loadedCheerio2('div.novel_text') + .find('p') + .first() + .text() + .trim(); + + const baseUrl = this.site; + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('li.chapter-item.unlock').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterPath = loadedCheerio(el).find('a').attr('href'); + if (chapterPath) { + const chapter: Plugin.ChapterItem = { + name: chapterName, + path: chapterPath.slice(baseUrl.length), + }; + chapters.push(chapter); + } + }); + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + // parse chapter text here + const body = await fetchApi(this.site + chapterPath); + const html = await body.text().then(r => r.replace(/>\s+</g, '><')); + + const loadedCheerio = loadCheerio(html); + + const chapterText = loadedCheerio('div.text_story'); + chapterText.find('div.ads_content').remove(); + + return chapterText.html() || ''; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + // get novels using the search term + + if (pageNo !== 1) return []; + const searchUrl = this.site + '/read'; + const formData = new FormData(); + formData.append('field-search', searchTerm); + + const results = await fetchApi(searchUrl, { + method: 'POST', + body: formData, + }); + + const body = await results.text(); + const loadedCheerio = loadCheerio(body); + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div.read_list-story-item').each((i, el) => { + const tempNovel = {} as Plugin.NovelItem; + const img = loadedCheerio(el) + .find('.item_thumb') + .find('img') + .first() + .attr('src'); + let path = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('href'); + path = path ? path.slice(this.site.length) : ''; + const title = loadedCheerio(el) + .find('.item_thumb') + .find('a') + .first() + .attr('title'); + tempNovel.name = title ? title : ''; + tempNovel.path = path; + tempNovel.cover = img; + novels.push(tempNovel); + }); + + // type SearchEntry = { + // title: string; + // thumbnail: string; + // permalink: string; + // }; + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/chapter/') + path; +} + +export default new LNTPlugin(); diff --git a/plugins/english/lnmtl.ts b/plugins/english/lnmtl.ts new file mode 100644 index 000000000..f8dae9be8 --- /dev/null +++ b/plugins/english/lnmtl.ts @@ -0,0 +1,437 @@ +import { Parser } from 'htmlparser2'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; + +class LnMTLPlugin implements Plugin.PagePlugin { + id = 'lnmtl'; + name = 'LnMTL'; + icon = 'src/en/lnmtl/icon.png'; + site = 'https://lnmtl.com/'; + version = '2.1.1'; + + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + orderBy: filters.order.value, + order: filters.sort.value, + filter: filters.storyStatus.value, + page: page.toString(), + }); + + const link = `${this.site}novel?${params.toString()}`; + const response = await fetchApi(link); + const html = await response.text(); + const baseUrl = this.site; + let state: ParsingState = ParsingState.Idle; + let tempNovel: Partial<Plugin.NovelItem> = {}; + const novels: Plugin.NovelItem[] = []; + + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class']?.includes('media-left')) { + state = ParsingState.Novel; + } + if (state !== ParsingState.Novel) return; + + switch (name) { + case 'a': + tempNovel.path = attribs['href'].replace(baseUrl, ''); + break; + case 'img': + tempNovel.name = attribs['alt']; + tempNovel.cover = attribs['src']; + break; + } + }, + onclosetag(name) { + if (name === 'div') { + if (tempNovel.path && tempNovel.name) { + novels.push(tempNovel as Plugin.NovelItem); + tempNovel = {}; + } + state = ParsingState.Idle; + } + }, + }); + + parser.write(html); + parser.end(); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const body = await fetchApi(this.site + novelPath); + const html = await body.text().then(r => r.replace(/>\s+</g, '><')); + + const novel: Partial<Plugin.SourceNovel> & { totalPages: number } = { + path: novelPath, + totalPages: 1, + chapters: [], + }; + + let state = ParsingState.Idle; + let panelValueCount = 0; + let listCount = 0; + let isAuthorKey = false; + let isStatusKey = false; + const summaryParts: string[] = []; + const genreArray: string[] = []; + const parser = new Parser({ + onopentag(name, attribs) { + switch (name) { + case 'img': + if (attribs['class']?.includes('img-rounded')) { + novel.name = attribs['title']; + novel.cover = attribs['src']; + } + break; + case 'dt': + state = ParsingState.InPanelKey; + break; + case 'dd': + state = ParsingState.InPanelValue; + panelValueCount++; + break; + case 'ul': + if (attribs['class']?.includes('list-inline')) { + listCount++; + } + break; + case 'li': + if (listCount === 1) { + state = ParsingState.InGenres; + } + break; + default: + if (attribs['class']) { + const map: Record<string, ParsingState> = { + description: ParsingState.InDescription, + source: ParsingState.InSource, + progress: ParsingState.Idle, + }; + state = map[attribs['class']] ?? state; + } + } + }, + ontext(data) { + switch (state) { + case ParsingState.InScript: + { + const volume = JSON.parse( + data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '', + ); + novel.totalPages = volume.length; + } + break; + case ParsingState.InDescription: + summaryParts.push(data.trim()); + summaryParts.push('\n\n'); + break; + case ParsingState.InSource: + summaryParts.push(data); + break; + case ParsingState.InPanelKey: + switch (data) { + case 'Authors': + isAuthorKey = true; + break; + case 'Current status': + isStatusKey = true; + break; + } + break; + case ParsingState.InPanelValue: + if (isAuthorKey && panelValueCount === 1) { + novel.author = (novel.author || '') + data.trim(); + isAuthorKey = false; + } else if (isStatusKey && panelValueCount === 2) { + novel.status = (novel.status || '') + data.trim(); + isStatusKey = false; + } + break; + case ParsingState.InGenres: + genreArray.push(data.trim()); + break; + } + }, + onclosetag(name) { + switch (name) { + case 'ul': + if (state === ParsingState.InGenres) { + state = ParsingState.Idle; + } + break; + case 'main': + state = ParsingState.InScript; + break; + case 'script': + if (state === ParsingState.InScript) { + state = ParsingState.Idle; + } + break; + } + }, + onend() { + novel.summary = summaryParts.join(''); + novel.genres = genreArray.join(', '); + }, + }); + + parser.write(html); + parser.end(); + + return novel as Plugin.SourceNovel & { totalPages: number }; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const result = await fetchApi(this.site + novelPath); + const html = await result.text().then(r => r.replace(/>\s+</g, '><')); + let state: ParsingState = ParsingState.Idle; + let volume: VolumeEntry = { + id: '', + title: '', + }; + const parser = new Parser({ + ontext(data) { + if (state === ParsingState.InScript) { + const volumes = JSON.parse( + data.match(/lnmtl.volumes = (.+])(?=;)/)![1] || '', + ); + volume = volumes[+page - 1]; + } + }, + onclosetag(name) { + if (name === 'main') { + state = ParsingState.InScript; + } + if (name === 'script') { + state = ParsingState.Idle; + } + }, + }); + + parser.write(html); + parser.end(); + + const chapter: Plugin.ChapterItem[] = []; + + await this.sleep(1000); + const volumeData = await fetchApi( + `${this.site}chapter?volumeId=${volume.id}`, + ); + const volumePage = await volumeData.json(); + const firstPage = volumePage.data.map((chapter: ChapterEntry) => ({ + name: `#${chapter.number} - ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })); + chapter.push(...firstPage); + + for (let i = 2; i <= volumePage.last_page; i++) { + await this.sleep(1000); + const chapterData = await fetchApi( + `${this.site}chapter?page=${i}&volumeId=${volume.id}`, + ); + const chapterInfo = await chapterData.json(); + + const chapterDetails = chapterInfo.data.map((chapter: ChapterEntry) => ({ + name: `#${chapter.number} ${chapter.title}`, + path: `chapter/${chapter.slug}`, + releaseTime: new Date(chapter.created_at).toISOString(), //converts time obtained to UTC +0, TODO: Make it not convert + })); + + chapter.push(...chapterDetails); + } + const chapters = chapter; + return { chapters }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const html = await result.text(); + + let state: ParsingState = ParsingState.Idle; + const chapterTextParts: string[] = []; + + type EscapeChar = '&' | '<' | '>' | '"' | "'"; + const escapeRegex = /[&<>"']/g; + const escapeMap: Record<EscapeChar, string> = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + const escapeHtml = (text: string): string => + escapeRegex.test(text) + ? ((escapeRegex.lastIndex = 0), + text.replace(escapeRegex, char => escapeMap[char as EscapeChar])) + : text; + + const parser = new Parser({ + onopentag(name, attribs) { + if (name === 'sentence' && attribs['class']?.includes('translated')) { + state = ParsingState.Chapter; + chapterTextParts.push('<p>'); + } + }, + onopentagname(name) { + if (name === 'nav') { + state = ParsingState.Idle; + } + }, + ontext(data) { + if (state === ParsingState.Chapter) { + chapterTextParts.push(escapeHtml(data)); + } + }, + onclosetag(name) { + if (name === 'sentence' && state === ParsingState.Chapter) { + chapterTextParts.push('</p>'); + state = ParsingState.Idle; + } + }, + }); + + parser.write(html); + parser.end(); + const chapterText = chapterTextParts.join(''); + return chapterText.replace(/„/g, '“'); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const html = await fetchApi(this.site) + .then(b => b.text()) + .then(r => r.replace(/>\s+</g, '><')); + + let state = ParsingState.Idle; + const listPart = new Set<string>(); + const parser = new Parser({ + onopentag(name, attribs) { + if ( + state === ParsingState.InFooter && + name === 'script' && + attribs['type']?.includes('application/javascript') + ) { + state = ParsingState.InScript; + } + }, + ontext(data) { + if (state === ParsingState.InScript) { + const match = data.match(/prefetch: '\/(.*?\.json)/); + if (match) { + listPart.add(match[1]); + } + } + }, + onclosetag(name) { + switch (name) { + case 'footer': + state = ParsingState.InFooter; + break; + case 'script': + if (state === ParsingState.InScript) { + state = ParsingState.Idle; + } + break; + } + }, + }); + + parser.write(html); + parser.end(); + + const search = await fetchApi( + `${this.site}${Array.from(listPart).join('')}`, + ); + const data = await search.json(); + + const nov = data.filter((res: { name: string }) => + res.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + const novels: Plugin.NovelItem[] = []; + nov.map((res: { name: string; slug: string; image: string }) => { + const novelName = res.name; + const novelUrl = `novel/${res.slug}`; + const novelCover = res.image; + + const novel = { + path: novelUrl, + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }); + return novels; + } + + filters = { + order: { + value: 'favourites', + label: 'Order by', + options: [ + { label: 'Favourites', value: 'favourites' }, + { label: 'Name', value: 'name' }, + { label: 'Addition Date', value: 'date' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'desc', + label: 'Sort by', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Finished', value: 'finished' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new LnMTLPlugin(); + +type ChapterEntry = { + number: number; + title: string; + slug: string; + created_at: string; +}; + +type VolumeEntry = { + id: string; + title: string; +}; + +enum ParsingState { + Idle, + InFooter, + InScript, + InDescription, + InSource, + InPanelKey, + InPanelValue, + InGenres, + Novel, + Chapter, +} diff --git a/plugins/english/mtlreader.broken.ts b/plugins/english/mtlreader.broken.ts new file mode 100644 index 000000000..e24a04517 --- /dev/null +++ b/plugins/english/mtlreader.broken.ts @@ -0,0 +1,101 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +class MTLReader implements Plugin.PluginBase { + id = 'mtlreader'; + name = 'MTL Reader'; + version = '1.0.1'; + icon = 'src/en/mtlreader/icon.png'; + site = 'https://mtlreader.com/'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.col-md-4').each((i, el) => { + const novelName = loadedCheerio(el).find('h5').text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el).find('h5 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}novels?page=${pageNo}`; + + const body = await fetchApi(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.agent-title').text().trim() || 'Untitled', + cover: loadedCheerio('.agent-p-img > img').attr('src'), + summary: loadedCheerio('#editdescription').text().trim(), + chapters: [], + }; + + novel.author = loadedCheerio('i.fa-user') + .parent() + .text() + .replace('Author: ', '') + .trim(); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('tr.spaceUnder').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + loadedCheerio('.container ins,script,p.mtlreader').remove(); + const chapterText = loadedCheerio('.container').html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const body = await fetchApi(this.site).then(r => r.text()); + const tokenCheerio = parseHTML(body); + const token = tokenCheerio('input[name="_token"]').attr('value'); + + const searchUrl = `${this.site}search?_token=${token}&input=${encodeURIComponent(searchTerm)}`; + const seacrhBody = await fetchApi(searchUrl).then(r => r.text()); + const loadedCheerio = parseHTML(seacrhBody); + return this.parseNovels(loadedCheerio); + } +} +export default new MTLReader(); diff --git a/plugins/english/mvlempyr.ts b/plugins/english/mvlempyr.ts new file mode 100644 index 000000000..dc7723b0a --- /dev/null +++ b/plugins/english/mvlempyr.ts @@ -0,0 +1,1245 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Parser } from 'htmlparser2'; + +//has to be here cus this scoping moment +// im just gonna disable eslint here instead of reading lol +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const parserData: { + inTag?: string; + depthRatingWrapper?: number; + depth: number; + ret?: { + path: string; + cover: string; + name?: string; + avgReview?: string; + reviewCount?: string; + chapterCount?: string; + created?: string; + genres?: string; + tags?: string; + }; + parser?: Parser; +} = { depth: 0 }; + +class MVLEMPYRPlugin implements Plugin.PluginBase { + id = 'mvlempyr.com'; + name = 'MVLEMPYR'; + icon = 'src/en/mvlempyr/icon.png'; + site = 'https://www.mvlempyr.io/'; + version = '1.0.12'; + + _chapSite = 'https://chap.heliosarchive.online/'; + _allNovels: (Plugin.NovelItem & ExtraNovelData)[] | undefined; + _allNovelsPromise: Promise<(Plugin.NovelItem & ExtraNovelData)[]> | undefined; + + checkCaptcha(loadedCheerio: CheerioAPI) { + const title = loadedCheerio('title').text(); + if ( + title === 'Attention Required! | Cloudflare' || + title === 'Just a moment...' + ) + throw new Error('Captcha error, please open in webview'); + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const data = await this.getAllNovels(); + const filtered = data.filter(novel => { + for (const genre of filters.genre.value.exclude || []) { + if (novel.genres.includes(genre)) { + return false; + } + } + + for (const genre of filters.genre.value.include || []) { + if (!novel.genres.includes(genre)) { + return false; + } + } + + for (const tag of filters.tag.value.exclude || []) { + if (novel.tags.includes(tag)) { + return false; + } + } + + for (const tag of filters.tag.value.include || []) { + if (!novel.tags.includes(tag)) { + return false; + } + } + + return true; + }); + const sortKey = filters?.order?.value || 'reviewCount'; + // @ts-ignore + const sorted = filtered.sort((a, b) => b[sortKey] - a[sortKey]); + + return this.paginate(sorted, pageNo); + } + + convertNovelId(e: bigint) { + const t = 1999999997n; + let u = 1n, + c = 7n % t, + d = e; + for ( + ; + d > 0; + (1n & d) === 1n && (u = (u * c) % t), c = (c * c) % t, d >>= 1n + ); + return u; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + this.checkCaptcha(loadedCheerio); + + const code = loadedCheerio('#novel-code').text(); + const newNovelId = this.convertNovelId(BigInt(parseInt(code))); + const firstPostsReq = await fetchApi( + this._chapSite + + 'wp-json/wp/v2/posts?tags=' + + newNovelId + + '&per_page=500&page=1', + ); + + const pages = parseInt(firstPostsReq.headers.get('X-Wp-Totalpages')) || 1; + + const posts = [ + await firstPostsReq.json(), + ...(await Promise.all( + new Array(pages - 1) + .fill(0) + .map((_, i) => i + 2) + .map(page => + fetchApi( + this._chapSite + + 'wp-json/wp/v2/posts?tags=' + + newNovelId + + '&per_page=500&page=' + + page, + ).then(res => res.json()), + ), + )), + ].flat(); + + return { + path: novelPath, + name: loadedCheerio('h1.novel-title').text() || 'Untitled', + cover: loadedCheerio('img.novel-image').attr('src'), + summary: loadedCheerio('div.synopsis.w-richtext').text().trim(), + chapters: posts + .map(chap => ({ + name: chap.acf.ch_name, + path: + 'chapter/' + chap.acf.novel_code + '-' + chap.acf.chapter_number, + releaseTime: chap.date, + chapterNumber: chap.acf.chapter_number, + })) + .reverse(), + status: loadedCheerio('.novelstatustextlarge').text(), + author: loadedCheerio( + 'div.additionalinfo.tm10 > div.textwrapper:nth-child(1)', + ) + .toArray() + .filter(e => { + return ( + e.children.length == 2 && + e.children[0].children[0].data === 'Author:' + ); + })[0].children[1].children[0].data, + genres: loadedCheerio('.genre-tags') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','), + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + this.checkCaptcha(loadedCheerio); + + return loadedCheerio('#chapter > span').html() || ''; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const allNovels = await this.getAllNovels(); + const searchResults = allNovels.filter(novel => + novel.name.toLowerCase().includes(searchTerm.toLowerCase()), + ); + + return this.paginate(searchResults, page); + } + + paginate<T>(data: T[], page: number): T[] { + const startIndex = (page - 1) * 20; + const endIndex = startIndex + 20; + return data.slice(startIndex, endIndex); + } + + async getAllNovels() { + if (this._allNovelsPromise) { + await this._allNovelsPromise; + } + if (this._allNovels) { + return this._allNovels; + } + this._allNovelsPromise = this.loadAll(); + try { + this._allNovels = await this._allNovelsPromise; + } finally { + this._allNovelsPromise = undefined; + } + return this._allNovels; + } + + async loadAll() { + const data = await fetchApi( + this._chapSite + 'wp-json/wp/v2/mvl-novels?per_page=10000', + ).then(r => r.json()); + // @ts-ignore + return data.map(novel => { + return { + name: novel.name, + path: 'novel/' + novel.slug, + cover: `https://assets.mvlempyr.app/images/600/${novel['novel-code']}.webp`, + avgReview: novel['average-review'], + reviewCount: novel['total-reviews'], + chapterCount: novel['total-chapters'], + created: new Date(novel['createdOn']).getTime(), + genres: novel.genre, + tags: novel.tags, + random: Math.random(), + }; + }); + } + + filters = { + order: { + type: FilterTypes.Picker, + value: 'reviewCount', + label: 'Order by', + options: [ + { label: 'Latest Added', value: 'created' }, + { label: 'Best Rated', value: 'avgReview' }, + { label: 'Most Reviewed', value: 'reviewCount' }, + { label: 'Chapter Count', value: 'chapterCount' }, + { label: 'Random', value: 'random' }, + ], + }, + // update genres and tags with this script if needed + // console.log([...$0.querySelectorAll("a")].map(a=>{ + // return [a.querySelector(".g-ttext").innerText, a.href.split("/tag/").pop()] + // }).map(([a, b])=>`{label: '${a.replace('\'', '\\\'')}', value: '${b}'},`).join("\n")) + genre: { + type: FilterTypes.ExcludableCheckboxGroup, + label: 'Genres', + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fan-Fiction', value: 'fan-fiction' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + }, + tag: { + type: FilterTypes.ExcludableCheckboxGroup, + label: 'Tags', + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Abandoned Children', value: 'abandoned-children' }, + { label: 'Ability Steal', value: 'ability-steal' }, + { label: 'Absent Parents', value: 'absent-parents' }, + { label: 'Abusive Characters', value: 'abusive-characters' }, + { label: 'Academy', value: 'academy' }, + { label: 'Accelerated Growth', value: 'accelerated-growth' }, + { label: 'Acting', value: 'acting' }, + { label: 'Adapted from Manga', value: 'adapted-from-manga' }, + { label: 'Adapted from Manhua', value: 'adapted-from-manhua' }, + { label: 'Adapted to Anime', value: 'adapted-to-anime' }, + { label: 'Adapted to Drama', value: 'adapted-to-drama' }, + { label: 'Adapted to Drama CD', value: 'adapted-to-drama-cd' }, + { label: 'Adapted to Game', value: 'adapted-to-game' }, + { label: 'Adapted to Manga', value: 'adapted-to-manga' }, + { label: 'Adapted to Manhua', value: 'adapted-to-manhua' }, + { label: 'Adapted to Manhwa', value: 'adapted-to-manhwa' }, + { label: 'Adapted to Movie', value: 'adapted-to-movie' }, + { label: 'Adapted to Visual Novel', value: 'adapted-to-visual-novel' }, + { label: 'Adopted Children', value: 'adopted-children' }, + { label: 'Adopted Protagonist', value: 'adopted-protagonist' }, + { label: 'Adultery', value: 'adultery' }, + { label: 'Advanced technology', value: 'advanced-technology' }, + { label: 'Adventurers', value: 'adventurers' }, + { label: 'Affair', value: 'affair' }, + { label: 'Age Progression', value: 'age-progression' }, + { label: 'Age Regression', value: 'age-regression' }, + { label: 'Aggressive Characters', value: 'aggressive-characters' }, + { label: 'Alchemy', value: 'alchemy' }, + { label: 'Aliens', value: 'aliens' }, + { label: 'All-Girls School', value: 'all-girls-school' }, + { label: 'Alternate World', value: 'alternate-world' }, + { label: 'American Comics', value: 'american-comics' }, + { label: 'Amnesia', value: 'amnesia' }, + { label: 'Amusement Park', value: 'amusement-park' }, + { label: 'An*l', value: 'an-l' }, + { label: 'Ancient China', value: 'ancient-china' }, + { label: 'Ancient Times', value: 'ancient-times' }, + { label: 'Androgynous Characters', value: 'androgynous-characters' }, + { label: 'Androids', value: 'androids' }, + { label: 'Angels', value: 'angels' }, + { label: 'Animal Characteristics', value: 'animal-characteristics' }, + { label: 'Animal Rearing', value: 'animal-rearing' }, + { label: 'Anti-Heo', value: 'anti-heo' }, + { label: 'Anti-Magic', value: 'anti-magic' }, + { label: 'Anti-social Protagonist', value: 'anti-social-protagonist' }, + { label: 'Antihero Protagonist', value: 'antihero-protagonist' }, + { label: 'Antique Shop', value: 'antique-shop' }, + { label: 'Apartment Life', value: 'apartment-life' }, + { label: 'Apathetic Protagonist', value: 'apathetic-protagonist' }, + { label: 'Apocalypse', value: 'apocalypse' }, + { label: 'Appearance Changes', value: 'appearance-changes' }, + { + label: 'Appearance Different from Actual Age', + value: 'appearance-different-from-actual-age', + }, + { label: 'Archery', value: 'archery' }, + { label: 'Aristocracy', value: 'aristocracy' }, + { label: 'Arms Dealers', value: 'arms-dealers' }, + { label: 'Army', value: 'army' }, + { label: 'Army Building', value: 'army-building' }, + { label: 'Arranged Marriage', value: 'arranged-marriage' }, + { label: 'Arrogant Characters', value: 'arrogant-characters' }, + { label: 'Artifact Crafting', value: 'artifact-crafting' }, + { label: 'Artifacts', value: 'artifacts' }, + { label: 'Artificial Intelligence', value: 'artificial-intelligence' }, + { label: 'Artists', value: 'artists' }, + { label: 'Assassins', value: 'assassins' }, + { label: 'Astrologers', value: 'astrologers' }, + { label: 'Autism', value: 'autism' }, + { label: 'Automatons', value: 'automatons' }, + { + label: 'Average-looking Protagonist', + value: 'average-looking-protagonist', + }, + { label: 'Award-winning Work', value: 'award-winning-work' }, + { label: 'Awkward Protagonist', value: 'awkward-protagonist' }, + { label: 'BDSM', value: 'bdsm' }, + { label: 'Bands', value: 'bands' }, + { label: 'Based on a Movie', value: 'based-on-a-movie' }, + { label: 'Based on a Song', value: 'based-on-a-song' }, + { label: 'Based on a TV Show', value: 'based-on-a-tv-show' }, + { label: 'Based on a Video Game', value: 'based-on-a-video-game' }, + { label: 'Based on a Visual Novel', value: 'based-on-a-visual-novel' }, + { label: 'Based on an Anime', value: 'based-on-an-anime' }, + { label: 'Battle Academy', value: 'battle-academy' }, + { label: 'Battle Competition', value: 'battle-competition' }, + { label: 'Beast Companions', value: 'beast-companions' }, + { label: 'Beastkin', value: 'beastkin' }, + { label: 'Beasts', value: 'beasts' }, + { label: 'Beautiful Female Lead', value: 'beautiful-female-lead' }, + { label: 'Bestiality', value: 'bestiality' }, + { label: 'Betrayal', value: 'betrayal' }, + { label: 'Bickering Couple', value: 'bickering-couple' }, + { label: 'Biochip', value: 'biochip' }, + { label: 'Bisexual Protagonist', value: 'bisexual-protagonist' }, + { label: 'Black Belly', value: 'black-belly' }, + { label: 'Blackmail', value: 'blackmail' }, + { label: 'Blacksmith', value: 'blacksmith' }, + { label: 'Blind Dates', value: 'blind-dates' }, + { label: 'Blind Protagonist', value: 'blind-protagonist' }, + { label: 'Blood Manipulation', value: 'blood-manipulation' }, + { label: 'Bloodlines', value: 'bloodlines' }, + { label: 'Body Swap', value: 'body-swap' }, + { label: 'Body Tempering', value: 'body-tempering' }, + { label: 'Body-double', value: 'body-double' }, + { label: 'Bodyguards', value: 'bodyguards' }, + { label: 'Books', value: 'books' }, + { label: 'Bookworm', value: 'bookworm' }, + { + label: 'Boss-Subordinate Relationship', + value: 'boss-subordinate-relationship', + }, + { label: 'Brainwashing', value: 'brainwashing' }, + { label: 'Breast Fetish', value: 'breast-fetish' }, + { label: 'Broken Engagement', value: 'broken-engagement' }, + { label: 'Brother Complex', value: 'brother-complex' }, + { label: 'Brotherhood', value: 'brotherhood' }, + { label: 'Buddhism', value: 'buddhism' }, + { label: 'Bullying', value: 'bullying' }, + { label: 'Business Management', value: 'business-management' }, + { label: 'Businessmen', value: 'businessmen' }, + { label: 'Butlers', value: 'butlers' }, + { label: 'C*nnilingus', value: 'c-nnilingus' }, + { label: 'Calm Protagonist', value: 'calm-protagonist' }, + { label: 'Cannibalism', value: 'cannibalism' }, + { label: 'Card Games', value: 'card-games' }, + { label: 'Carefree Protagonist', value: 'carefree-protagonist' }, + { label: 'Caring Protagonist', value: 'caring-protagonist' }, + { label: 'Cautious Protagonist', value: 'cautious-protagonist' }, + { label: 'Celebrities', value: 'celebrities' }, + { label: 'Character Growth', value: 'character-growth' }, + { label: 'Charismatic Protagonist', value: 'charismatic-protagonist' }, + { label: 'Charming Protagonist', value: 'charming-protagonist' }, + { label: 'Chat Rooms', value: 'chat-rooms' }, + { label: 'Cheats', value: 'cheats' }, + { label: 'Chefs', value: 'chefs' }, + { label: 'Child Abuse', value: 'child-abuse' }, + { label: 'Child Protagonist', value: 'child-protagonist' }, + { label: 'Childcare', value: 'childcare' }, + { label: 'Childhood Friends', value: 'childhood-friends' }, + { label: 'Childhood Love', value: 'childhood-love' }, + { label: 'Childhood Promise', value: 'childhood-promise' }, + { label: 'Childish Protagonist', value: 'childish-protagonist' }, + { label: 'Chuunibyou', value: 'chuunibyou' }, + { label: 'Clan Building', value: 'clan-building' }, + { label: 'Classic', value: 'classic' }, + { label: 'Clever Protagonist', value: 'clever-protagonist' }, + { label: 'Clingy Lover', value: 'clingy-lover' }, + { label: 'Clones', value: 'clones' }, + { label: 'Clubs', value: 'clubs' }, + { label: 'Clumsy Love Interests', value: 'clumsy-love-interests' }, + { label: 'Co-Workers', value: 'co-workers' }, + { label: 'Cohabitation', value: 'cohabitation' }, + { label: 'Cold Love Interests', value: 'cold-love-interests' }, + { label: 'Cold Protagonist', value: 'cold-protagonist' }, + { + label: 'Collection of Short Stories', + value: 'collection-of-short-stories', + }, + { label: 'College/University', value: 'college-university' }, + { label: 'Coma', value: 'coma' }, + { label: 'Comedic Undertone', value: 'comedic-undertone' }, + { label: 'Coming of Age', value: 'coming-of-age' }, + { + label: 'Complex Family Relationships', + value: 'complex-family-relationships', + }, + { label: 'Conditional Power', value: 'conditional-power' }, + { label: 'Confident Protagonist', value: 'confident-protagonist' }, + { label: 'Confinement', value: 'confinement' }, + { label: 'Conflicting Loyalties', value: 'conflicting-loyalties' }, + { label: 'Contracts', value: 'contracts' }, + { label: 'Cooking', value: 'cooking' }, + { label: 'Corruption', value: 'corruption' }, + { label: 'Cosmic Wars', value: 'cosmic-wars' }, + { label: 'Cosplay', value: 'cosplay' }, + { label: 'Couple Growth', value: 'couple-growth' }, + { label: 'Court Official', value: 'court-official' }, + { label: 'Cousins', value: 'cousins' }, + { label: 'Cowardly Protagonist', value: 'cowardly-protagonist' }, + { label: 'Crafting', value: 'crafting' }, + { label: 'Crime', value: 'crime' }, + { label: 'Criminals', value: 'criminals' }, + { label: 'Cross-dressing', value: 'cross-dressing' }, + { label: 'Crossover', value: 'crossover' }, + { label: 'Cruel Characters', value: 'cruel-characters' }, + { label: 'Cryostasis', value: 'cryostasis' }, + { label: 'Cultivation', value: 'cultivation' }, + { label: 'Cunning Protagonist', value: 'cunning-protagonist' }, + { label: 'Curious Protagonist', value: 'curious-protagonist' }, + { label: 'Curses', value: 'curses' }, + { label: 'Cute Children', value: 'cute-children' }, + { label: 'Cute Protagonist', value: 'cute-protagonist' }, + { label: 'Cute Story', value: 'cute-story' }, + { label: 'DC', value: 'dc' }, + { label: 'Dancers', value: 'dancers' }, + { label: 'Dao Companion', value: 'dao-companion' }, + { label: 'Dao Comprehension', value: 'dao-comprehension' }, + { label: 'Daoism', value: 'daoism' }, + { label: 'Dark', value: 'dark' }, + { label: 'Dead Protagonist', value: 'dead-protagonist' }, + { label: 'Death', value: 'death' }, + { label: 'Death of Loved Ones', value: 'death-of-loved-ones' }, + { label: 'Debts', value: 'debts' }, + { label: 'Delinquents', value: 'delinquents' }, + { label: 'Delusions', value: 'delusions' }, + { label: 'Demi-Humans', value: 'demi-humans' }, + { label: 'Demon Lord', value: 'demon-lord' }, + { + label: 'Demonic Cultivation Technique', + value: 'demonic-cultivation-technique', + }, + { label: 'Demons', value: 'demons' }, + { label: 'Dense Protagonist', value: 'dense-protagonist' }, + { label: 'Depictions of Cruelty', value: 'depictions-of-cruelty' }, + { label: 'Depression', value: 'depression' }, + { label: 'Destiny', value: 'destiny' }, + { label: 'Detectives', value: 'detectives' }, + { label: 'Determined Protagonist', value: 'determined-protagonist' }, + { label: 'Devoted Love Interests', value: 'devoted-love-interests' }, + { label: 'Different Social Status', value: 'different-social-status' }, + { label: 'Disabilities', value: 'disabilities' }, + { label: 'Discrimination', value: 'discrimination' }, + { label: 'Disfigurement', value: 'disfigurement' }, + { label: 'Dishonest Protagonist', value: 'dishonest-protagonist' }, + { label: 'Distrustful Protagonist', value: 'distrustful-protagonist' }, + { label: 'Divination', value: 'divination' }, + { label: 'Divine Protection', value: 'divine-protection' }, + { label: 'Divorce', value: 'divorce' }, + { label: 'Doctors', value: 'doctors' }, + { label: 'Dolls/Puppets', value: 'dolls-puppets' }, + { label: 'Domestic Affairs', value: 'domestic-affairs' }, + { label: 'Doting Love Interests', value: 'doting-love-interests' }, + { label: 'Doting Older Siblings', value: 'doting-older-siblings' }, + { label: 'Doting Parents', value: 'doting-parents' }, + { label: 'Dragon Ball', value: 'dragon-ball' }, + { label: 'Dragon Riders', value: 'dragon-riders' }, + { label: 'Dragon Slayers', value: 'dragon-slayers' }, + { label: 'Dragons', value: 'dragons' }, + { label: 'Dreams', value: 'dreams' }, + { label: 'Drugs', value: 'drugs' }, + { label: 'Druids', value: 'druids' }, + { label: 'Dungeon Master', value: 'dungeon-master' }, + { label: 'Dungeons', value: 'dungeons' }, + { label: 'Dwarfs', value: 'dwarfs' }, + { label: 'Dystopia', value: 'dystopia' }, + { label: 'Early Romance', value: 'early-romance' }, + { label: 'Earth Invasion', value: 'earth-invasion' }, + { label: 'Easy Going Life', value: 'easy-going-life' }, + { label: 'Economics', value: 'economics' }, + { label: 'Editors', value: 'editors' }, + { label: 'Eidetic Memory', value: 'eidetic-memory' }, + { label: 'Elderly Protagonist', value: 'elderly-protagonist' }, + { label: 'Elemental Magic', value: 'elemental-magic' }, + { label: 'Elves', value: 'elves' }, + { + label: 'Emotionally Weak Protagonist', + value: 'emotionally-weak-protagonist', + }, + { label: 'Empires', value: 'empires' }, + { label: 'Enemies Become Allies', value: 'enemies-become-allies' }, + { label: 'Enemies Become Lovers', value: 'enemies-become-lovers' }, + { label: 'Engagement', value: 'engagement' }, + { label: 'Engineer', value: 'engineer' }, + { label: 'Enlightenment', value: 'enlightenment' }, + { label: 'Episodic', value: 'episodic' }, + { label: 'Eunuch', value: 'eunuch' }, + { label: 'European Ambience', value: 'european-ambience' }, + { label: 'Evil Gods', value: 'evil-gods' }, + { label: 'Evil Organizations', value: 'evil-organizations' }, + { label: 'Evil Protagonist', value: 'evil-protagonist' }, + { label: 'Evil Religions', value: 'evil-religions' }, + { label: 'Evolution', value: 'evolution' }, + { label: 'Exhibitionism', value: 'exhibitionism' }, + { label: 'Exorcism', value: 'exorcism' }, + { label: 'Eye Powers', value: 'eye-powers' }, + { label: 'F*llatio', value: 'f-llatio' }, + { label: 'Face slapping', value: 'face-slapping' }, + { label: 'Fairies', value: 'fairies' }, + { label: 'Fallen Angels', value: 'fallen-angels' }, + { label: 'Fallen Nobility', value: 'fallen-nobility' }, + { label: 'Familial Love', value: 'familial-love' }, + { label: 'Familiars', value: 'familiars' }, + { label: 'Family', value: 'family' }, + { label: 'Family Business', value: 'family-business' }, + { label: 'Family Conflict', value: 'family-conflict' }, + { label: 'Famous Parents', value: 'famous-parents' }, + { label: 'Famous Protagonist', value: 'famous-protagonist' }, + { label: 'Fanaticism', value: 'fanaticism' }, + { label: 'Fanfiction', value: 'fanfiction' }, + { label: 'Fantasy Creatures', value: 'fantasy-creatures' }, + { label: 'Fantasy World', value: 'fantasy-world' }, + { label: 'Farming', value: 'farming' }, + { label: 'Fast Cultivation', value: 'fast-cultivation' }, + { label: 'Fast Learner', value: 'fast-learner' }, + { label: 'Fat Protagonist', value: 'fat-protagonist' }, + { label: 'Fat to Fit', value: 'fat-to-fit' }, + { label: 'Fated Lovers', value: 'fated-lovers' }, + { label: 'Fearless Protagonist', value: 'fearless-protagonist' }, + { label: 'Female Master', value: 'female-master' }, + { label: 'Female Protagonist', value: 'female-protagonist' }, + { label: 'Female to Male', value: 'female-to-male' }, + { label: 'Feng Shui', value: 'feng-shui' }, + { label: 'Firearms', value: 'firearms' }, + { label: 'First Love', value: 'first-love' }, + { label: 'First-time Interc**rse', value: 'first-time-interc-rse' }, + { label: 'Flashbacks', value: 'flashbacks' }, + { label: 'Fleet Battles', value: 'fleet-battles' }, + { label: 'Folklore', value: 'folklore' }, + { + label: 'Forced Living Arrangements', + value: 'forced-living-arrangements', + }, + { label: 'Forced Marriage', value: 'forced-marriage' }, + { + label: 'Forced into a Relationship', + value: 'forced-into-a-relationship', + }, + { label: 'Forgetful Protagonist', value: 'forgetful-protagonist' }, + { label: 'Former Hero', value: 'former-hero' }, + { label: 'Fox Spirits', value: 'fox-spirits' }, + { label: 'Friends Become Enemies', value: 'friends-become-enemies' }, + { label: 'Friendship', value: 'friendship' }, + { label: 'Fujoshi', value: 'fujoshi' }, + { label: 'Futanari', value: 'futanari' }, + { label: 'Futuristic Setting', value: 'futuristic-setting' }, + { label: 'Galge', value: 'galge' }, + { label: 'Gambling', value: 'gambling' }, + { label: 'Game Elements', value: 'game-elements' }, + { label: 'Game Ranking System', value: 'game-ranking-system' }, + { label: 'Gamers', value: 'gamers' }, + { label: 'Gangs', value: 'gangs' }, + { label: 'Gate to Another World', value: 'gate-to-another-world' }, + { label: 'Genderless Protagonist', value: 'genderless-protagonist' }, + { label: 'Generals', value: 'generals' }, + { label: 'Genetic Modifications', value: 'genetic-modifications' }, + { label: 'Genies', value: 'genies' }, + { label: 'Genius Protagonist', value: 'genius-protagonist' }, + { label: 'Ghosts', value: 'ghosts' }, + { label: 'Gladiators', value: 'gladiators' }, + { + label: 'Glasses-wearing Love Interests', + value: 'glasses-wearing-love-interests', + }, + { + label: 'Glasses-wearing Protagonist', + value: 'glasses-wearing-protagonist', + }, + { label: 'Goblins', value: 'goblins' }, + { label: 'God Protagonist', value: 'god-protagonist' }, + { label: 'God-human Relationship', value: 'god-human-relationship' }, + { label: 'Goddesses', value: 'goddesses' }, + { label: 'Godly Powers', value: 'godly-powers' }, + { label: 'Gods', value: 'gods' }, + { label: 'Golems', value: 'golems' }, + { label: 'Gore', value: 'gore' }, + { label: 'Grave Keepers', value: 'grave-keepers' }, + { label: 'Grinding', value: 'grinding' }, + { label: 'Guardian Relationship', value: 'guardian-relationship' }, + { label: 'Guilds', value: 'guilds' }, + { label: 'Gunfighters', value: 'gunfighters' }, + { label: 'H*ndjob', value: 'h-ndjob' }, + { label: 'Hackers', value: 'hackers' }, + { label: 'Half-human Protagonist', value: 'half-human-protagonist' }, + { label: 'Handsome Male Lead', value: 'handsome-male-lead' }, + { + label: 'Hard-Working Protagonist', + value: 'hard-working-protagonist', + }, + { + label: 'Harem-seeking Protagonist', + value: 'harem-seeking-protagonist', + }, + { label: 'Harry Potter', value: 'harry-potter' }, + { label: 'Harsh Training', value: 'harsh-training' }, + { label: 'Hated Protagonist', value: 'hated-protagonist' }, + { label: 'Healers', value: 'healers' }, + { label: 'Heartwarming', value: 'heartwarming' }, + { label: 'Heaven', value: 'heaven' }, + { label: 'Heavenly Tribulation', value: 'heavenly-tribulation' }, + { label: 'Hell', value: 'hell' }, + { label: 'Helpful Protagonist', value: 'helpful-protagonist' }, + { label: 'Herbalist', value: 'herbalist' }, + { label: 'Heroes', value: 'heroes' }, + { label: 'Heterochromia', value: 'heterochromia' }, + { label: 'Hidden Abilities', value: 'hidden-abilities' }, + { label: 'Hiding True Abilities', value: 'hiding-true-abilities' }, + { label: 'Hiding True Identity', value: 'hiding-true-identity' }, + { label: 'Hikikomori', value: 'hikikomori' }, + { label: 'Hollywood', value: 'hollywood' }, + { label: 'Homunculus', value: 'homunculus' }, + { label: 'Honest Protagonist', value: 'honest-protagonist' }, + { label: 'Hospital', value: 'hospital' }, + { label: 'Hot-blooded Protagonist', value: 'hot-blooded-protagonist' }, + { label: 'Human Experimentation', value: 'human-experimentation' }, + { label: 'Human Weapon', value: 'human-weapon' }, + { + label: 'Human-Nonhuman Relationship', + value: 'human-nonhuman-relationship', + }, + { label: 'Humanoid Protagonist', value: 'humanoid-protagonist' }, + { label: 'Hunter x Hunter', value: 'hunter-x-hunter' }, + { label: 'Hunters', value: 'hunters' }, + { label: 'Hypnotism', value: 'hypnotism' }, + { label: 'Identity Crisis', value: 'identity-crisis' }, + { label: 'Imaginary Friend', value: 'imaginary-friend' }, + { label: 'Immortals', value: 'immortals' }, + { label: 'Imperial Harem', value: 'imperial-harem' }, + { label: 'Incest', value: 'incest' }, + { label: 'Incubus', value: 'incubus' }, + { label: 'Indecisive Protagonist', value: 'indecisive-protagonist' }, + { label: 'Industrialization', value: 'industrialization' }, + { label: 'Inferiority Complex', value: 'inferiority-complex' }, + { label: 'Inheritance', value: 'inheritance' }, + { label: 'Inscriptions', value: 'inscriptions' }, + { label: 'Insects', value: 'insects' }, + { + label: 'Interconnected Storylines', + value: 'interconnected-storylines', + }, + { label: 'Interdimensional Travel', value: 'interdimensional-travel' }, + { label: 'Introverted Protagonist', value: 'introverted-protagonist' }, + { label: 'Investigations', value: 'investigations' }, + { label: 'Invisibility', value: 'invisibility' }, + { label: 'JSDF', value: 'jsdf' }, + { label: 'Jack of All Trades', value: 'jack-of-all-trades' }, + { label: 'Jealousy', value: 'jealousy' }, + { label: 'Jiangshi', value: 'jiangshi' }, + { label: 'Jobless Class', value: 'jobless-class' }, + { label: 'Jujutsu Kaisen', value: 'jujutsu-kaisen' }, + { label: 'Kidnappings', value: 'kidnappings' }, + { label: 'Kind Love Interests', value: 'kind-love-interests' }, + { label: 'Kingdom Building', value: 'kingdom-building' }, + { label: 'Kingdoms', value: 'kingdoms' }, + { label: 'Knights', value: 'knights' }, + { label: 'Kuudere', value: 'kuudere' }, + { label: 'Lack of Common Sense', value: 'lack-of-common-sense' }, + { label: 'Language Barrier', value: 'language-barrier' }, + { label: 'Late Romance', value: 'late-romance' }, + { label: 'Lawyers', value: 'lawyers' }, + { label: 'Lazy Protagonist', value: 'lazy-protagonist' }, + { label: 'Leadership', value: 'leadership' }, + { label: 'Legends', value: 'legends' }, + { label: 'Level System', value: 'level-system' }, + { label: 'Library', value: 'library' }, + { label: 'Limited Lifespan', value: 'limited-lifespan' }, + { label: 'Living Abroad', value: 'living-abroad' }, + { label: 'Living Alone', value: 'living-alone' }, + { label: 'Loli', value: 'loli' }, + { label: 'Loneliness', value: 'loneliness' }, + { label: 'Loner Protagonist', value: 'loner-protagonist' }, + { label: 'Long Separations', value: 'long-separations' }, + { + label: 'Long-distance Relationship', + value: 'long-distance-relationship', + }, + { label: 'Lost Civilizations', value: 'lost-civilizations' }, + { label: 'Lottery', value: 'lottery' }, + { + label: 'Love Interest Falls in Love First', + value: 'love-interest-falls-in-love-first', + }, + { label: 'Love Rivals', value: 'love-rivals' }, + { label: 'Love Triangles', value: 'love-triangles' }, + { label: 'Love at First Sight', value: 'love-at-first-sight' }, + { label: 'Lovers Reunited', value: 'lovers-reunited' }, + { label: 'Low-key Protagonist', value: 'low-key-protagonist' }, + { label: 'Loyal Subordinates', value: 'loyal-subordinates' }, + { label: 'Lucky Protagonist', value: 'lucky-protagonist' }, + { label: 'M*sturbation', value: 'm-sturbation' }, + { label: 'MMORPG', value: 'mmorpg' }, + { label: 'Mafia', value: 'mafia' }, + { label: 'Magic', value: 'magic' }, + { label: 'Magic Beasts', value: 'magic-beasts' }, + { label: 'Magic Formations', value: 'magic-formations' }, + { label: 'Magical Girls', value: 'magical-girls' }, + { label: 'Magical Space', value: 'magical-space' }, + { label: 'Magical Technology', value: 'magical-technology' }, + { label: 'Maids', value: 'maids' }, + { label: 'Male Protagonist', value: 'male-protagonist' }, + { label: 'Male Yandere', value: 'male-yandere' }, + { label: 'Male to Female', value: 'male-to-female' }, + { label: 'Management', value: 'management' }, + { label: 'Mangaka', value: 'mangaka' }, + { label: 'Manipulative Characters', value: 'manipulative-characters' }, + { label: 'Manly Gay Couple', value: 'manly-gay-couple' }, + { label: 'Marriage', value: 'marriage' }, + { label: 'Marriage of Convenience', value: 'marriage-of-convenience' }, + { label: 'Martial Spirits', value: 'martial-spirits' }, + { label: 'Marvel', value: 'marvel' }, + { label: 'Masochistic Characters', value: 'masochistic-characters' }, + { + label: 'Master-Disciple Relationship', + value: 'master-disciple-relationship', + }, + { + label: 'Master-Servant Relationship', + value: 'master-servant-relationship', + }, + { label: 'Matriarchy', value: 'matriarchy' }, + { label: 'Mature Protagonist', value: 'mature-protagonist' }, + { label: 'Medical Knowledge', value: 'medical-knowledge' }, + { label: 'Medieval', value: 'medieval' }, + { label: 'Mercenaries', value: 'mercenaries' }, + { label: 'Merchants', value: 'merchants' }, + { label: 'Military', value: 'military' }, + { label: 'Mind Break', value: 'mind-break' }, + { label: 'Mind Control', value: 'mind-control' }, + { label: 'Misandry', value: 'misandry' }, + { label: 'Mismatched Couple', value: 'mismatched-couple' }, + { label: 'Misunderstandings', value: 'misunderstandings' }, + { label: 'Mob Protagonist', value: 'mob-protagonist' }, + { label: 'Models', value: 'models' }, + { label: 'Modern Day', value: 'modern-day' }, + { label: 'Modern Knowledge', value: 'modern-knowledge' }, + { label: 'Money Grubber', value: 'money-grubber' }, + { label: 'Monster Girls', value: 'monster-girls' }, + { label: 'Monster Society', value: 'monster-society' }, + { label: 'Monster Tamer', value: 'monster-tamer' }, + { label: 'Monsters', value: 'monsters' }, + { label: 'Movies', value: 'movies' }, + { label: 'Mpreg', value: 'mpreg' }, + { label: 'Multiple Identities', value: 'multiple-identities' }, + { label: 'Multiple POV', value: 'multiple-pov' }, + { label: 'Multiple Personalities', value: 'multiple-personalities' }, + { label: 'Multiple Protagonists', value: 'multiple-protagonists' }, + { label: 'Multiple Realms', value: 'multiple-realms' }, + { + label: 'Multiple Reincarnated Individuals', + value: 'multiple-reincarnated-individuals', + }, + { label: 'Multiple Timelines', value: 'multiple-timelines' }, + { + label: 'Multiple Transported Individuals', + value: 'multiple-transported-individuals', + }, + { label: 'Murders', value: 'murders' }, + { label: 'Music', value: 'music' }, + { label: 'Mutated Creatures', value: 'mutated-creatures' }, + { label: 'Mutations', value: 'mutations' }, + { label: 'Mute Character', value: 'mute-character' }, + { + label: 'Mysterious Family Background', + value: 'mysterious-family-background', + }, + { label: 'Mysterious Illness', value: 'mysterious-illness' }, + { label: 'Mysterious Past', value: 'mysterious-past' }, + { label: 'Mystery Solving', value: 'mystery-solving' }, + { label: 'Mythical Beasts', value: 'mythical-beasts' }, + { label: 'Mythology', value: 'mythology' }, + { label: 'Naive Protagonist', value: 'naive-protagonist' }, + { + label: 'Narcissistic Protagonist', + value: 'narcissistic-protagonist', + }, + { label: 'Naruto', value: 'naruto' }, + { label: 'Nationalism', value: 'nationalism' }, + { label: 'Near-Death Experience', value: 'near-death-experience' }, + { label: 'Necromancer', value: 'necromancer' }, + { label: 'Neet', value: 'neet' }, + { label: 'Netorare', value: 'netorare' }, + { label: 'Netorase', value: 'netorase' }, + { label: 'Netori', value: 'netori' }, + { label: 'Nightmares', value: 'nightmares' }, + { label: 'Ninjas', value: 'ninjas' }, + { label: 'Nobles', value: 'nobles' }, + { + label: 'Non-humanoid Protagonist', + value: 'non-humanoid-protagonist', + }, + { label: 'Non-linear Storytelling', value: 'non-linear-storytelling' }, + { label: 'Not-harem', value: 'not-harem' }, + { label: 'Nudity', value: 'nudity' }, + { label: 'Nurses', value: 'nurses' }, + { label: 'Obsessive Love', value: 'obsessive-love' }, + { label: 'Office Romance', value: 'office-romance' }, + { label: 'Older Love Interests', value: 'older-love-interests' }, + { label: 'Omegaverse', value: 'omegaverse' }, + { label: 'One Piece', value: 'one-piece' }, + { label: 'Oneshot', value: 'oneshot' }, + { label: 'Online Romance', value: 'online-romance' }, + { label: 'Onmyouji', value: 'onmyouji' }, + { label: 'Or*y', value: 'or-y' }, + { label: 'Orcs', value: 'orcs' }, + { label: 'Organized Crime', value: 'organized-crime' }, + { label: 'Orphans', value: 'orphans' }, + { label: 'Otaku', value: 'otaku' }, + { label: 'Otome Game', value: 'otome-game' }, + { label: 'Outcasts', value: 'outcasts' }, + { label: 'Outdoor Interc**rse', value: 'outdoor-interc-rse' }, + { label: 'Outer Space', value: 'outer-space' }, + { label: 'Overpowered Protagonist', value: 'overpowered-protagonist' }, + { label: 'Overprotective Siblings', value: 'overprotective-siblings' }, + { label: 'Pacifist Protagonist', value: 'pacifist-protagonist' }, + { label: 'Paizuri', value: 'paizuri' }, + { label: 'Parallel Worlds', value: 'parallel-worlds' }, + { label: 'Parasites', value: 'parasites' }, + { label: 'Parent Complex', value: 'parent-complex' }, + { label: 'Parody', value: 'parody' }, + { label: 'Part-Time Job', value: 'part-time-job' }, + { label: 'Past Plays a Big Role', value: 'past-plays-a-big-role' }, + { label: 'Past Trauma', value: 'past-trauma' }, + { label: 'Pe*verted Protagonist', value: 'pe-verted-protagonist' }, + { + label: 'Persistent Love Interests', + value: 'persistent-love-interests', + }, + { label: 'Personality Changes', value: 'personality-changes' }, + { label: 'Pets', value: 'pets' }, + { label: 'Pharmacist', value: 'pharmacist' }, + { label: 'Philosophical', value: 'philosophical' }, + { label: 'Phobias', value: 'phobias' }, + { label: 'Phoenixes', value: 'phoenixes' }, + { label: 'Photography', value: 'photography' }, + { label: 'Pill Based Cultivation', value: 'pill-based-cultivation' }, + { label: 'Pill Concocting', value: 'pill-concocting' }, + { label: 'Pilots', value: 'pilots' }, + { label: 'Pirates', value: 'pirates' }, + { label: 'Playboys', value: 'playboys' }, + { label: 'Playful Protagonist', value: 'playful-protagonist' }, + { label: 'Poetry', value: 'poetry' }, + { label: 'Poisons', value: 'poisons' }, + { label: 'Police', value: 'police' }, + { label: 'Polite Protagonist', value: 'polite-protagonist' }, + { label: 'Politics', value: 'politics' }, + { label: 'Polyandry', value: 'polyandry' }, + { label: 'Polygamy', value: 'polygamy' }, + { label: 'Poor Protagonist', value: 'poor-protagonist' }, + { label: 'Poor to Rich', value: 'poor-to-rich' }, + { label: 'Popular Love Interests', value: 'popular-love-interests' }, + { label: 'Possession', value: 'possession' }, + { label: 'Possessive Characters', value: 'possessive-characters' }, + { label: 'Post-apocalyptic', value: 'post-apocalyptic' }, + { label: 'Power Couple', value: 'power-couple' }, + { label: 'Power Struggle', value: 'power-struggle' }, + { label: 'Pragmatic Protagonist', value: 'pragmatic-protagonist' }, + { label: 'Precognition', value: 'precognition' }, + { label: 'Pregnancy', value: 'pregnancy' }, + { label: 'Pretend Lovers', value: 'pretend-lovers' }, + { label: 'Previous Life Talent', value: 'previous-life-talent' }, + { label: 'Priestesses', value: 'priestesses' }, + { label: 'Priests', value: 'priests' }, + { label: 'Prison', value: 'prison' }, + { label: 'Proactive Protagonist', value: 'proactive-protagonist' }, + { label: 'Programmer', value: 'programmer' }, + { label: 'Prophecies', value: 'prophecies' }, + { label: 'Prostit**es', value: 'prostit-es' }, + { + label: 'Protagonist Falls in Love First', + value: 'protagonist-falls-in-love-first', + }, + { + label: 'Protagonist Strong from the Start', + value: 'protagonist-strong-from-the-start', + }, + { + label: 'Protagonist with Multiple Bodies', + value: 'protagonist-with-multiple-bodies', + }, + { label: 'Psychic Powers', value: 'psychic-powers' }, + { label: 'Psychopaths', value: 'psychopaths' }, + { label: 'Puppeteers', value: 'puppeteers' }, + { label: 'Quiet Characters', value: 'quiet-characters' }, + { label: 'Quirky Characters', value: 'quirky-characters' }, + { label: 'R*pe', value: 'r-pe' }, + { + label: 'R*pe Victim Becomes Lover', + value: 'r-pe-victim-becomes-lover', + }, + { label: 'R-15', value: 'r-15' }, + { label: 'R-18', value: 'r-18' }, + { label: 'Race Change', value: 'race-change' }, + { label: 'Racism', value: 'racism' }, + { label: 'Rebellion', value: 'rebellion' }, + { + label: 'Reincarnated as a Monster', + value: 'reincarnated-as-a-monster', + }, + { + label: 'Reincarnated as an Object', + value: 'reincarnated-as-an-object', + }, + { + label: 'Reincarnated in Another World', + value: 'reincarnated-in-another-world', + }, + { + label: 'Reincarnated in a Game World', + value: 'reincarnated-in-a-game-world', + }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Religions', value: 'religions' }, + { label: 'Reluctant Protagonist', value: 'reluctant-protagonist' }, + { label: 'Reporters', value: 'reporters' }, + { label: 'Restaurant', value: 'restaurant' }, + { label: 'Resurrection', value: 'resurrection' }, + { + label: 'Returning from Another World', + value: 'returning-from-another-world', + }, + { label: 'Revenge', value: 'revenge' }, + { label: 'Reverse Harem', value: 'reverse-harem' }, + { label: 'Reverse R*pe', value: 'reverse-r-pe' }, + { label: 'Reversible Couple', value: 'reversible-couple' }, + { label: 'Rich to Poor', value: 'rich-to-poor' }, + { label: 'Righteous Protagonist', value: 'righteous-protagonist' }, + { label: 'Rivalry', value: 'rivalry' }, + { label: 'Romantic Subplot', value: 'romantic-subplot' }, + { label: 'Roommates', value: 'roommates' }, + { label: 'Royalty', value: 'royalty' }, + { label: 'Ruthless Protagonist', value: 'ruthless-protagonist' }, + { label: 'S*ave Harem', value: 's-ave-harem' }, + { label: 'S*ave Protagonist', value: 's-ave-protagonist' }, + { label: 'S*aves', value: 's-aves' }, + { label: 'S*x Friends', value: 's-x-friends' }, + { label: 'S*x S*aves', value: 's-x-s-aves' }, + { label: 'S*xual Abuse', value: 's-xual-abuse' }, + { + label: 'S*xual Cultivation Technique', + value: 's-xual-cultivation-technique', + }, + { label: 'Sadistic Characters', value: 'sadistic-characters' }, + { label: 'Saints', value: 'saints' }, + { label: 'Salaryman', value: 'salaryman' }, + { label: 'Samurai', value: 'samurai' }, + { label: 'Saving the World', value: 'saving-the-world' }, + { + label: 'Schemes And Conspiracies', + value: 'schemes-and-conspiracies', + }, + { label: 'Schizophrenia', value: 'schizophrenia' }, + { label: 'Scientists', value: 'scientists' }, + { label: 'Sculptors', value: 'sculptors' }, + { label: 'Sealed Power', value: 'sealed-power' }, + { label: 'Second Chance', value: 'second-chance' }, + { label: 'Secret Crush', value: 'secret-crush' }, + { label: 'Secret Identity', value: 'secret-identity' }, + { label: 'Secret Organizations', value: 'secret-organizations' }, + { label: 'Secret Relationship', value: 'secret-relationship' }, + { label: 'Secretive Protagonist', value: 'secretive-protagonist' }, + { label: 'Secrets', value: 'secrets' }, + { label: 'Sect Development', value: 'sect-development' }, + { label: 'Seduction', value: 'seduction' }, + { + label: "Seeing Things Other Humans Can't", + value: 'seeing-things-other-humans-cant', + }, + { label: 'Selfish Protagonist', value: 'selfish-protagonist' }, + { label: 'Selfless Protagonist', value: 'selfless-protagonist' }, + { label: 'Seme Protagonist', value: 'seme-protagonist' }, + { + label: 'Senpai-Kouhai Relationship', + value: 'senpai-kouhai-relationship', + }, + { label: 'Sentient Objects', value: 'sentient-objects' }, + { label: 'Sentimental Protagonist', value: 'sentimental-protagonist' }, + { label: 'Serial Killers', value: 'serial-killers' }, + { label: 'Servants', value: 'servants' }, + { label: 'Seven Deadly Sins', value: 'seven-deadly-sins' }, + { label: 'Seven Virtues', value: 'seven-virtues' }, + { label: 'Shameless Protagonist', value: 'shameless-protagonist' }, + { label: 'Shapeshifters', value: 'shapeshifters' }, + { label: 'Sharing A Body', value: 'sharing-a-body' }, + { + label: 'Sharp-tongued Characters', + value: 'sharp-tongued-characters', + }, + { label: 'Shield User', value: 'shield-user' }, + { label: 'Shikigami', value: 'shikigami' }, + { label: 'Short Story', value: 'short-story' }, + { label: 'Shota', value: 'shota' }, + { label: 'Shoujo-Ai Subplot', value: 'shoujo-ai-subplot' }, + { label: 'Shounen-Ai Subplot', value: 'shounen-ai-subplot' }, + { label: 'Showbiz', value: 'showbiz' }, + { label: 'Shy Characters', value: 'shy-characters' }, + { label: 'Sibling Rivalry', value: 'sibling-rivalry' }, + { label: "Sibling's Care", value: 'siblings-care' }, + { label: 'Siblings', value: 'siblings' }, + { + label: 'Siblings Not Related by Blood', + value: 'siblings-not-related-by-blood', + }, + { label: 'Sickly Characters', value: 'sickly-characters' }, + { label: 'Sign Language', value: 'sign-language' }, + { label: 'Singers', value: 'singers' }, + { label: 'Single Parent', value: 'single-parent' }, + { label: 'Sister Complex', value: 'sister-complex' }, + { label: 'Skill Assimilation', value: 'skill-assimilation' }, + { label: 'Skill Books', value: 'skill-books' }, + { label: 'Skill Creation', value: 'skill-creation' }, + { label: 'Sleeping', value: 'sleeping' }, + { label: 'Slow Growth at Start', value: 'slow-growth-at-start' }, + { label: 'Slow Romance', value: 'slow-romance' }, + { label: 'Smart Couple', value: 'smart-couple' }, + { label: 'Social Outcasts', value: 'social-outcasts' }, + { label: 'Soldiers', value: 'soldiers' }, + { label: 'Soul Power', value: 'soul-power' }, + { label: 'Souls', value: 'souls' }, + { label: 'Spatial Manipulation', value: 'spatial-manipulation' }, + { label: 'Spear Wielder', value: 'spear-wielder' }, + { label: 'Special Abilities', value: 'special-abilities' }, + { label: 'Spies', value: 'spies' }, + { label: 'Spirit Advisor', value: 'spirit-advisor' }, + { label: 'Spirit Users', value: 'spirit-users' }, + { label: 'Spirits', value: 'spirits' }, + { label: 'Stalkers', value: 'stalkers' }, + { label: 'Stockholm Syndrome', value: 'stockholm-syndrome' }, + { label: 'Stoic Characters', value: 'stoic-characters' }, + { label: 'Store Owner', value: 'store-owner' }, + { label: 'Straight Seme', value: 'straight-seme' }, + { label: 'Straight Uke', value: 'straight-uke' }, + { label: 'Strategic Battles', value: 'strategic-battles' }, + { label: 'Strategist', value: 'strategist' }, + { + label: 'Strength-based Social Hierarchy', + value: 'strength-based-social-hierarchy', + }, + { label: 'Strong Background', value: 'strong-background' }, + { label: 'Strong Love Interests', value: 'strong-love-interests' }, + { label: 'Strong to Stronger', value: 'strong-to-stronger' }, + { label: 'Stubborn Protagonist', value: 'stubborn-protagonist' }, + { label: 'Student Council', value: 'student-council' }, + { + label: 'Student-Teacher Relationship', + value: 'student-teacher-relationship', + }, + { label: 'Succubus', value: 'succubus' }, + { label: 'Sudden Strength Gain', value: 'sudden-strength-gain' }, + { label: 'Sudden Wealth', value: 'sudden-wealth' }, + { label: 'Suicides', value: 'suicides' }, + { label: 'Summoned Hero', value: 'summoned-hero' }, + { label: 'Summoning Magic', value: 'summoning-magic' }, + { label: 'Survival', value: 'survival' }, + { label: 'Survival Game', value: 'survival-game' }, + { label: 'Sword And Magic', value: 'sword-and-magic' }, + { label: 'Sword Wielder', value: 'sword-wielder' }, + { label: 'System', value: 'system' }, + { label: 'System Administrator', value: 'system-administrator' }, + { label: 'Teachers', value: 'teachers' }, + { label: 'Teamwork', value: 'teamwork' }, + { label: 'Technological Gap', value: 'technological-gap' }, + { label: 'Tentacles', value: 'tentacles' }, + { label: 'Terminal Illness', value: 'terminal-illness' }, + { label: 'Territory Management', value: 'territory-management' }, + { label: 'Terrorists', value: 'terrorists' }, + { label: 'Thieves', value: 'thieves' }, + { label: 'Threesome', value: 'threesome' }, + { label: 'Thriller', value: 'thriller' }, + { label: 'Time Loop', value: 'time-loop' }, + { label: 'Time Manipulation', value: 'time-manipulation' }, + { label: 'Time Paradox', value: 'time-paradox' }, + { label: 'Time Skip', value: 'time-skip' }, + { label: 'Time Travel', value: 'time-travel' }, + { label: 'Timid Protagonist', value: 'timid-protagonist' }, + { label: 'Tomboyish Female Lead', value: 'tomboyish-female-lead' }, + { label: 'Torture', value: 'torture' }, + { label: 'Toys', value: 'toys' }, + { label: 'Tragic Past', value: 'tragic-past' }, + { label: 'Transformation Ability', value: 'transformation-ability' }, + { label: 'Transmigration', value: 'transmigration' }, + { label: 'Transplanted Memories', value: 'transplanted-memories' }, + { + label: 'Transported Modern Structure', + value: 'transported-modern-structure', + }, + { + label: 'Transported into a Game World', + value: 'transported-into-a-game-world', + }, + { + label: 'Transported to Another World', + value: 'transported-to-another-world', + }, + { label: 'Trap', value: 'trap' }, + { label: 'Tribal Society', value: 'tribal-society' }, + { label: 'Trickster', value: 'trickster' }, + { label: 'Tsundere', value: 'tsundere' }, + { label: 'Twins', value: 'twins' }, + { label: 'Twisted Personality', value: 'twisted-personality' }, + { label: 'Ugly Protagonist', value: 'ugly-protagonist' }, + { label: 'Ugly to Beautiful', value: 'ugly-to-beautiful' }, + { label: 'Unconditional Love', value: 'unconditional-love' }, + { + label: 'Underestimated Protagonist', + value: 'underestimated-protagonist', + }, + { + label: 'Unique Cultivation Technique', + value: 'unique-cultivation-technique', + }, + { label: 'Unique Weapon User', value: 'unique-weapon-user' }, + { label: 'Unique Weapons', value: 'unique-weapons' }, + { label: 'Unlimited Flow', value: 'unlimited-flow' }, + { label: 'Unlucky Protagonist', value: 'unlucky-protagonist' }, + { label: 'Unreliable Narrator', value: 'unreliable-narrator' }, + { label: 'Unrequited Love', value: 'unrequited-love' }, + { label: 'Valkyries', value: 'valkyries' }, + { label: 'Vampires', value: 'vampires' }, + { label: 'Villain', value: 'villain' }, + { label: 'Villainess Noble Girls', value: 'villainess-noble-girls' }, + { label: 'Virtual Reality', value: 'virtual-reality' }, + { label: 'Vocaloid', value: 'vocaloid' }, + { label: 'Voice Actors', value: 'voice-actors' }, + { label: 'Voyeurism', value: 'voyeurism' }, + { label: 'Waiters', value: 'waiters' }, + { label: 'War Records', value: 'war-records' }, + { label: 'Wars', value: 'wars' }, + { label: 'Weak Protagonist', value: 'weak-protagonist' }, + { label: 'Weak to Strong', value: 'weak-to-strong' }, + { label: 'Wealthy Characters', value: 'wealthy-characters' }, + { label: 'Werebeasts', value: 'werebeasts' }, + { label: 'Wishes', value: 'wishes' }, + { label: 'Witches', value: 'witches' }, + { label: 'Wizards', value: 'wizards' }, + { label: 'World Hopping', value: 'world-hopping' }, + { label: 'World Travel', value: 'world-travel' }, + { label: 'World Tree', value: 'world-tree' }, + { label: 'Writers', value: 'writers' }, + { label: 'Yandere', value: 'yandere' }, + { label: 'Youkai', value: 'youkai' }, + { label: 'Younger Brothers', value: 'younger-brothers' }, + { label: 'Younger Love Interests', value: 'younger-love-interests' }, + { label: 'Younger Sisters', value: 'younger-sisters' }, + { label: 'Zombies', value: 'zombies' }, + { label: 'e-Sports', value: 'e-sports' }, + ], + }, + } satisfies Filters; +} + +type ExtraNovelData = { + avgReview: number; + reviewCount: number; + chapterCount: number; + created: number; + genres: string[]; + tags: string[]; +}; + +export default new MVLEMPYRPlugin(); diff --git a/plugins/english/novelbuddy.ts b/plugins/english/novelbuddy.ts new file mode 100644 index 000000000..da81763d6 --- /dev/null +++ b/plugins/english/novelbuddy.ts @@ -0,0 +1,372 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; + +const fwnRegex = + /(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|\bf)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ |b)(?:ո|ռ|ח|𝒏|𝓷|𝙣|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|延|𝐧|𝔫|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|オ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|\|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝙫|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ḷ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\||\\|Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|ލ|𝗅|𝗹|ލ|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖈|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖈|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ం|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝒐|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|o|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|告知|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝕞|𝕞|ⓜ|m|ന|ᙢ|൩|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|🇲|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g; + +class NovelBuddy implements Plugin.PluginBase { + id = 'novelbuddy'; + name = 'NovelBuddy'; + site = 'https://novelbuddy.com/'; + api = 'https://api.novelbuddy.com/'; + version = '2.1.2'; + icon = 'src/en/novelbuddy/icon.png'; + + parseNovels(body: Response): Plugin.NovelItem[] { + return body.data.items.map(item => ({ + name: item.name, + path: item.url.startsWith('/') ? item.url.slice(1) : item.url, + cover: item.cover, + })); + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const { genre, min_ch, max_ch, status, demo, orderBy, keyword } = filters; + + const parseNumber = (val?: string) => { + if (!val?.trim()) return; + const n = Number(val); + return Number.isInteger(n) && n >= 0 && n <= 10000 + ? String(n) + : undefined; + }; + + const rawParams: Record<string, string | undefined> = { + genres: genre.value.include?.join(',') || undefined, + exclude: genre.value.exclude?.join(',') || undefined, + min_ch: parseNumber(min_ch.value), + max_ch: parseNumber(max_ch.value), + status: status.value !== 'all' ? String(status.value) : undefined, + demographic: demo.value?.join(',') || undefined, + sort: String(orderBy.value), + page: String(pageNo), + limit: '24', + q: keyword.value || undefined, + }; + + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(rawParams)) { + if (value !== undefined) params.append(key, value); + } + + const url = this.api + 'titles/search?' + params.toString(); + const result = await fetchApi(url); + const body = await result.json(); + + return this.parseNovels(body); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const response = await fetchApi(this.site + novelPath); + const body = await response.text(); + + const scriptMatch = body.match( + /<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/, + ); + if (!scriptMatch) throw new Error('Could not find __NEXT_DATA__'); + + const data: NovelScript = JSON.parse(scriptMatch[1]); + const initialManga = data.props.pageProps.initialManga; + if (!initialManga) throw new Error('Could not find initialManga data'); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: initialManga.name || 'Untitled', + cover: initialManga.cover, + author: initialManga.authors?.map(a => a.name).join(', ') || '', + artist: initialManga.artists?.map(a => a.name).join(', ') || '', + genres: initialManga.genres?.map(g => g.name).join(',') || '', + chapters: [], + }; + + const rawStatus = initialManga.status; + const map: Record<string, string> = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + }; + novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown; + + const summaryStr = initialManga.summary || ''; + if (summaryStr) { + const $ = parseHTML('<div>' + summaryStr + '</div>'); + $('br').replaceWith('\n'); + $('p').before('\n').after('\n\n'); + novel.summary = $('div') + .text() + .split('\n') + .map(line => line.trim()) + .filter(line => line.length > 0) + .join('\n\n') + .trim(); + } + + if (initialManga.ratingStats) { + novel.rating = initialManga.ratingStats.average; + } + + const cv = initialManga.content_version || initialManga.cv; + const chaptersUrl = `${this.api}titles/${initialManga.id}/chapters${ + cv ? `?cv=${cv}` : '' + }`; + const chaptersResponse = await fetchApi(chaptersUrl); + const chaptersJson: ChapterResponse = await chaptersResponse.json(); + + if (chaptersJson?.success && chaptersJson?.data?.chapters) { + novel.chapters = chaptersJson.data.chapters + .map(chapter => ({ + name: chapter.name, + path: + (chapter.url.startsWith('/') ? chapter.url.slice(1) : chapter.url) + + `?id=${initialManga.id}&chapterId=${chapter.id}`, + releaseTime: chapter.updated_at, + })) + .reverse(); + } else if (initialManga.chapters) { + novel.chapters = initialManga.chapters + .map(chapter => ({ + name: chapter.name, + path: chapter.url.startsWith('/') + ? chapter.url.slice(1) + : chapter.url, + releaseTime: chapter.updatedAt, + })) + .reverse(); + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const novelIdMatch = chapterPath.match(/[?&]id=([^&]+)/); + const chapterIdMatch = chapterPath.match(/[?&]chapterId=([^&]+)/); + + let content = ''; + + if (novelIdMatch && chapterIdMatch) { + const novelId = novelIdMatch[1]; + const chapterId = chapterIdMatch[1]; + const apiUrl = `${this.api}titles/${novelId}/chapters/${chapterId}`; + const response = await fetchApi(apiUrl); + const json = await response.json(); + content = json?.data?.chapter?.content || ''; + } + + if (!content) { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + const scriptMatch = body.match( + /<script id="__NEXT_DATA__" type="application\/json">(.*?)<\/script>/, + ); + if (!scriptMatch) throw new Error('Could not find __NEXT_DATA__'); + + const data: ChapterScript = JSON.parse(scriptMatch[1]); + const initialChapter = data.props.pageProps.initialChapter; + if (!initialChapter) throw new Error('Could not find chapter content'); + content = initialChapter.content; + } + + if (content) { + content = content.replace( + /Find authorized novels in Webnovel.*?faster updates, better experience.*?Please click www\.webnovel\.com for visiting\./gi, + '', + ); + content = content.replace(fwnRegex, ''); + } + + return content; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + 'q': searchTerm, + 'limit': '24', + 'page': page.toString(), + }); + const url = this.api + 'titles/search?' + params.toString(); + const result = await fetchApi(url); + const body = await result.json(); + return this.parseNovels(body); + } + + filters = { + orderBy: { + value: 'views', + label: 'Order by', + options: [ + { label: 'Default Order', value: '' }, + { label: 'Most Viewed', value: 'views' }, + { label: 'Latest Updated', value: 'latest' }, + { label: 'Most Popular', value: 'popular' }, + { label: 'A-Z', value: 'alphabetical' }, + { label: 'Highest Rating', value: 'rating' }, + { label: 'Most Chapters', value: 'chapters' }, + ], + type: FilterTypes.Picker, + }, + keyword: { value: '', label: 'Keywords', type: FilterTypes.TextInput }, + status: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Hiatus', value: 'hiatus' }, + { label: 'Cancelled', value: 'cancelled' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: { include: [], exclude: [] }, + label: 'Genres (OR, not AND)', + options: [ + { label: 'Action', value: 'action' }, + { label: 'ActionAdventure', value: 'actionadventure' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern', value: 'eastern' }, + { label: 'Easterni', value: 'easterni' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fan-Fiction', value: 'fan-fiction' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Game', value: 'game' }, + { label: 'Games', value: 'games' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Josei', value: 'josei' }, + { label: 'Lolicon', value: 'lolicon' }, + { label: 'Magic', value: 'magic' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Military', value: 'military' }, + { label: 'Modern Life', value: 'modern-life' }, + { label: 'Movies', value: 'movies' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychologic', value: 'psychologic' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Reincarnatio', value: 'reincarnatio' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Romanc', value: 'romanc' }, + { label: 'Romance', value: 'romance' }, + { label: 'Romance.Adventure', value: 'romance-adventure' }, + { label: 'RomanceAdventure', value: 'romanceadventure' }, + { label: 'Romance.Harem', value: 'romance-harem' }, + { label: 'RomanceHarem', value: 'romanceharem' }, + { label: 'Romance.Smut', value: 'romance-smut' }, + { label: 'Romancei', value: 'romancei' }, + { label: 'Romancem', value: 'romancem' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Seinen Wuxia', value: 'seinen-wuxia' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Slice of Lif', value: 'slice-of-lif' }, + { label: 'Slice Of Life', value: 'slice-of-life' }, + { label: 'Slice of Lifel', value: 'slice-of-lifel' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Superna', value: 'superna' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'System', value: 'system' }, + { label: 'Thriller', value: 'thriller' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Urban', value: 'urban' }, + { label: 'Urban Life', value: 'urban-life' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + min_ch: { + value: '', + label: 'Minimum Chapters', + type: FilterTypes.TextInput, + }, + max_ch: { + label: 'Maximum Chapters', + value: '', + type: FilterTypes.TextInput, + }, + type: { + value: '', + label: 'Types', + options: [ + { label: 'All Types', value: '' }, + { label: 'Japanese comics', value: 'manga' }, + { label: 'Korean comics', value: 'manhwa' }, + { label: 'Chinese comics', value: 'manhua' }, + ], + type: FilterTypes.Picker, + }, + demo: { + value: [], + label: 'Demographics', + options: [ + { label: 'Shounen', value: 'shounen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Josei', value: 'josei' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new NovelBuddy(); + +type Response = { data: { items: Items[] } }; +type ChapterResponse = { success: boolean; data?: { chapters?: Items[] } }; +type Items = { + id: string; + url: string; + name: string; + alt_name?: string; + cover?: string; + slug: string; + updated_at?: string; + updatedAt?: string; + cv?: number; +}; +type NovelScript = { props: { pageProps: { initialManga: Manga } } }; +type Manga = { + id: string; + url: string; + name?: string; + altName?: string; + cover: string; + status: string; + ratingStats?: { average: number }; + summary?: string; + artists?: { name: string; slug: string }[]; + authors?: { name: string; slug: string }[]; + genres?: { name: string; slug: string }[]; + chapters?: Items[]; + cv?: number; + content_version?: number; +}; +type ChapterScript = { props: { pageProps: { initialChapter: Chapter } } }; +type Chapter = { name: string; content: string }; diff --git a/plugins/english/novelfire.ts b/plugins/english/novelfire.ts new file mode 100644 index 000000000..97caa7b8d --- /dev/null +++ b/plugins/english/novelfire.ts @@ -0,0 +1,611 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@/types/constants'; +import { storage } from '@libs/storage'; + +class NovelFire implements Plugin.PluginBase { + id = 'novelfire'; + name = 'Novel Fire'; + version = '1.4.2'; + icon = 'src/en/novelfire/icon.png'; + site = 'https://novelfire.net/'; + webStorageUtilized = true; + novelList: string[] = []; + draw = 0; + + pluginSettings = { + pageLength: { + value: '', + label: 'Page Mode (Change if Broken)', + type: 'Switch', + }, + singlePage: { + value: '', + label: + 'Force load all chapters on a single page (Slower & use more data)', + type: 'Switch', + }, + }; + singlePage = storage.get('singlePage'); + pageLength = storage.get('pageLength'); + + async getCheerio(url: string, search: boolean): Promise<CheerioAPI> { + const r = await fetchApi(url); + if (!r.ok && search != true) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const $ = load(await r.text()); + + if ($('title').text().includes('Cloudflare')) { + throw new Error('Cloudflare is blocking requests. Try again later.'); + } + + return $; + } + + parseNovels( + loadedCheerio: CheerioAPI, + selector = '.novel-item', + ): Plugin.NovelItem[] { + return loadedCheerio(selector) + .map((_, el) => { + const $el = loadedCheerio(el); + const titleElement = $el.find('.novel-title > a'); + const fallbackElement = $el.find('a'); + + const novelName = + titleElement.text() || + fallbackElement.attr('title') || + 'No Title Found'; + + const imgElement = $el.find('.novel-cover > img'); + const rawSrc = imgElement.attr('data-src') ?? imgElement.attr('src'); + const novelCover = rawSrc + ? new URL(rawSrc, this.site).href + : defaultCover; + + const novelPath = + titleElement.attr('href') || fallbackElement.attr('href'); + + if (!novelPath) return null; + + return { + name: novelName, + cover: novelCover, + path: new URL(novelPath, this.site).pathname.substring(1), + }; + }) + .get() + .filter(novel => novel !== null) + .filter(novel => { + if (this.novelList.includes(novel.path)) { + return false; + } else { + this.novelList.push(novel.path); + return true; + } + }); + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + if (pageNo === 1) { + this.novelList = []; + this.draw = 0; + } + const url = this.site + 'search-adv'; + const params = new URLSearchParams(); + + for (const language of filters.language.value) { + params.append('country_id[]', language); + } + params.append('ctgcon', filters.genre_operator.value); + for (const genre of filters.genres.value) { + params.append('categories[]', genre); + } + params.append('totalchapter', filters.chapters.value); + params.append('ratcon', filters.rating_operator.value); + params.append('rating', filters.rating.value); + params.append('status', filters.status.value); + params.append('sort', showLatestNovels ? 'date' : filters.sort.value); + params.append('tagcon', 'and'); + params.append('page', pageNo.toString()); + + const loadedCheerio = await this.getCheerio( + `${url}?${params.toString()}`, + false, + ); + + return this.parseNovels(loadedCheerio); + } + + async getAllChapters( + novelPath: string, + post_id: string, + page: string, + ): Promise<Plugin.ChapterItem[]> { + const length = this.pageLength ? 100 : -1; + const url = `${this.site}ajax/listChapterDataAjax`; + const start = length === -1 ? 0 : (parseInt(page) - 1) * length; + this.draw++; + const params = new URLSearchParams({ + draw: this.draw.toString(), + 'columns[0][data]': 'n_sort', + 'columns[0][name]': 'cmm_posts_detail.n_sort', + 'columns[0][searchable]': 'true', + 'columns[0][orderable]': 'true', + 'columns[0][search][value]': '', + 'columns[0][search][regex]': 'false', + + 'columns[1][data]': 'bookmark_created_at', + 'columns[1][name]': 'bookmark_chapters.created_at', + 'columns[1][searchable]': 'false', + 'columns[1][orderable]': 'true', + 'columns[1][search][value]': '', + 'columns[1][search][regex]': 'false', + + 'order[0][column]': '0', + 'order[0][dir]': 'asc', + 'order[0][name]': 'cmm_posts_detail.n_sort', + + start: start.toString(), + length: length.toString(), + 'search[value]': '', + 'search[regex]': 'false', + post_id: post_id, + only_bookmark: 'false', + _: Date.now().toString(), + }); + + const result = await fetchApi(`${url}?${params.toString()}`); + if (result.status === 429) throw new NovelFireThrottlingError(); + + const body = await result.text(); + if (body.includes('You are being rate limited')) + throw new NovelFireThrottlingError(); + if (body.includes('Page Not Found 404')) throw new NovelFireAjaxNotFound(); + + return (JSON.parse(body).data || []) + .flatMap( + (idx: { title?: string; slug: string; n_sort: string | number }) => { + const name = load(idx.title || idx.slug) + .text() + .replace(/[\u200B-\u200D\uFEFF]/g, '') + .trim(); + const num = Number(idx.n_sort); + + return name && !isNaN(num) + ? [ + { + name, + path: `${novelPath}/chapter-${num}`, + chapterNumber: num, + }, + ] + : []; + }, + ) + .sort( + (a: Plugin.ChapterItem, b: Plugin.ChapterItem) => + (a.chapterNumber || 0) - (b.chapterNumber || 0), + ); + } + + async getAllChaptersForce( + novelPath: string, + pages: number, + ): Promise<Plugin.ChapterItem[]> { + const pagesArray = Array.from({ length: pages }, (_, i) => i + 1); + const allChapters: Plugin.ChapterItem[] = []; + + // When pages > ~30, we get rate limited. To mitigate, split into chunks and retry chunk on rate limit with delay. + const chunkSize = 5; // 5 pages per chunk was tested to be a good balance between speed and rate limiting. + const retryCount = 10; + const sleepTime = 3.5; // Rate limit seems to be around ~10s, so usually 3 retries should be enough for another ~30 pages. + + const chaptersArray: Plugin.SourcePage[] = []; + + for (let i = 0; i < pagesArray.length; i += chunkSize) { + const pagesArrayChunk = pagesArray.slice(i, i + chunkSize); + + const firstPage = pagesArrayChunk[0]; + const lastPage = pagesArrayChunk[pagesArrayChunk.length - 1]; + + let attempt = 0; + + while (attempt < retryCount) { + try { + // Parse all pages in chunk in parallel + const chaptersArrayChunk = await Promise.all( + pagesArrayChunk.map(page => + this.parsePage(novelPath, page.toString()), + ), + ); + + chaptersArray.push(...chaptersArrayChunk); + break; + } catch (err) { + if (err instanceof NovelFireThrottlingError) { + attempt += 1; + console.warn( + `[pages=${firstPage}-${lastPage}] Novel Fire is rate limiting requests. Retry attempt ${attempt + 1} in ${sleepTime} seconds...`, + ); + if (attempt === retryCount) { + throw err; + } + + // Sleep for X second before retrying + await new Promise(resolve => setTimeout(resolve, sleepTime * 1000)); + } else { + throw err; + } + } + } + } + + // Merge all chapters into a single array + for (const page of chaptersArray) { + if (page.chapters) { + for (const chapter of page.chapters) { + allChapters.push(chapter); + } + } + } + return allChapters; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + this.draw = 0; + const $ = await this.getCheerio(this.site + novelPath, false); + const baseUrl = this.site; + + const post_id = $('#novel-report').attr('report-post_id'); + if (post_id) { + storage.set(`${this.id}_${novelPath.split('/').pop()}`, post_id); + } + + const novel: Partial<Plugin.SourceNovel & { totalPages: number }> = { + path: novelPath, + totalPages: 1, + }; + + novel.name = + $('.novel-title').text().trim() ?? + $('.cover > img').attr('alt') ?? + 'No Titled Found'; + const coverUrl = + $('.cover > img').attr('data-src') ?? $('.cover > img').attr('src'); + + if (coverUrl) { + novel.cover = new URL(coverUrl, baseUrl).href; + } else { + novel.cover = defaultCover; + } + + novel.genres = $('.categories .property-item') + .map((_, el) => $(el).text()) + .toArray() + .join(','); + + const summary = $('.summary .content'); + summary.find('.expand').remove(); + summary.find('br').replaceWith('\n'); + summary.find('p').before('\n').after('\n\n'); + + novel.summary = + summary + .text() + .split('\n') + .map(line => line.trim()) + .join('\n') + ?.replace(/\n{3,}/g, '\n\n') + .trim() || 'Summary Not Found'; + + novel.author = $('.author .property-item > span').text(); + + const rawStatus = + $('.header-stats .ongoing').text() || + $('.header-stats .completed').text() || + 'Unknown'; + const map: Record<string, string> = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + unknown: NovelStatus.Unknown, + }; + novel.status = map[rawStatus.toLowerCase()] ?? NovelStatus.Unknown; + + novel.rating = parseFloat($('.nub').text().trim()); + + const totalChapters = $('.header-stats i.icon-book-open') + .parent() + .text() + .trim(); + const length = this.pageLength ? 100 : -1; + novel.totalPages = + length === -1 ? 1 : Math.ceil(parseInt(totalChapters) / length) || 1; + if (novel.totalPages === 1 && post_id) { + novel.chapters = await this.getAllChapters(novelPath, post_id, '1'); + } + if (length === 100 && this.singlePage) { + novel.chapters = await this.getAllChaptersForce( + novel.path as string, + novel.totalPages, + ); + novel.totalPages = 1; + } + + return novel as Plugin.SourceNovel & { totalPages: number }; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const post_id = storage.get(`${this.id}_${novelPath.split('/').pop()}`); + + if (post_id && !isNaN(Number(post_id))) { + try { + const chapters = await this.getAllChapters( + novelPath, + String(post_id), + page, + ); + return { chapters }; + } catch (e) { + // Fallback to scraping if AJAX fails + } + } + + // Fallback only works for multiples of 100 + const length = this.pageLength ? 100 : -1; + if (length === 100) { + const url = `${this.site}${novelPath}/chapters?page=${page}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = load(body); + + const chapters = loadedCheerio('.chapter-list li') + .map((_, ele) => { + const chapterName = + loadedCheerio(ele).find('a').attr('title') || 'No Title Found'; + const chapterPath = loadedCheerio(ele).find('a').attr('href'); + + if (!chapterPath) return null; + + return { + name: chapterName, + path: new URL(chapterPath, this.site).pathname.substring(1), + }; + }) + .get() + .filter(chapter => chapter !== null) as Plugin.ChapterItem[]; + + return { + chapters, + }; + } + + return { + chapters: [], + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + const loadedCheerio = await this.getCheerio(url, false); + + const chapterText = loadedCheerio('#content'); + const odds = chapterText.find( + ':not(p, h1, span, i, b, u, img, a, div, strong)', + ); + for (const ele of odds.toArray()) { + const tag = ele.name.toString(); + if (tag.length > 5 && ele.name.toString().substring(0, 1) == 'nf') { + loadedCheerio(ele).remove(); + } + } + + return chapterText.html()?.replace(/ /g, ' ') || ''; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + if (page === 1) { + this.novelList = []; + this.draw = 0; + } + const params = new URLSearchParams(); + params.append('keyword', searchTerm); + params.append('page', page.toString()); + const url = `${this.site}search?${params.toString()}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = load(body); + + return this.parseNovels(loadedCheerio, '.novel-list.chapters .novel-item'); + } + + filters = { + sort: { + label: 'Sort Results By', + value: 'rank-top', + options: [ + { label: 'Rank (Top)', value: 'rank-top' }, + { label: 'Rating Score (Top)', value: 'rating-score-top' }, + { label: 'Review Count (Most)', value: 'review' }, + { label: 'Comment Count (Most)', value: 'comment' }, + { label: 'Bookmark Count (Most)', value: 'bookmark' }, + { label: 'Today Views (Most)', value: 'today-view' }, + { label: 'Monthly Views (Most)', value: 'monthly-view' }, + { label: 'Total Views (Most)', value: 'total-view' }, + { label: 'Title (A>Z)', value: 'abc' }, + { label: 'Title (Z>A)', value: 'cba' }, + { label: 'Last Updated (Newest)', value: 'date' }, + { label: 'Chapter Count (Most)', value: 'chapter-count-most' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Translation Status', + value: '-1', + options: [ + { label: 'All', value: '-1' }, + { label: 'Completed', value: '1' }, + { label: 'Ongoing', value: '0' }, + ], + type: FilterTypes.Picker, + }, + genre_operator: { + label: 'Genres (And/Or/Exclude)', + value: 'and', + options: [ + { label: 'AND', value: 'and' }, + { label: 'OR', value: 'or' }, + { label: 'EXCLUDE', value: 'exclude' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + value: [], + options: [ + { label: 'Action', value: '3' }, + { label: 'Adult', value: '28' }, + { label: 'Adventure', value: '4' }, + { label: 'Anime', value: '46' }, + { label: 'Arts', value: '47' }, + { label: 'Comedy', value: '5' }, + { label: 'Drama', value: '24' }, + { label: 'Eastern', value: '44' }, + { label: 'Ecchi', value: '26' }, + { label: 'Fan-fiction', value: '48' }, + { label: 'Fantasy', value: '6' }, + { label: 'Game', value: '19' }, + { label: 'Gender Bender', value: '25' }, + { label: 'Harem', value: '7' }, + { label: 'Historical', value: '12' }, + { label: 'Horror', value: '37' }, + { label: 'Isekai', value: '49' }, + { label: 'Josei', value: '2' }, + { label: 'Lgbt+', value: '45' }, + { label: 'Magic', value: '50' }, + { label: 'Magical Realism', value: '51' }, + { label: 'Manhua', value: '52' }, + { label: 'Martial Arts', value: '15' }, + { label: 'Mature', value: '8' }, + { label: 'Mecha', value: '34' }, + { label: 'Military', value: '53' }, + { label: 'Modern Life', value: '54' }, + { label: 'Movies', value: '55' }, + { label: 'Mystery', value: '16' }, + { label: 'Other', value: '64' }, + { label: 'Psychological', value: '9' }, + { label: 'Realistic Fiction', value: '56' }, + { label: 'Reincarnation', value: '43' }, + { label: 'Romance', value: '1' }, + { label: 'School Life', value: '21' }, + { label: 'Sci-fi', value: '20' }, + { label: 'Seinen', value: '10' }, + { label: 'Shoujo', value: '38' }, + { label: 'Shoujo Ai', value: '57' }, + { label: 'Shounen', value: '17' }, + { label: 'Shounen Ai', value: '39' }, + { label: 'Slice of Life', value: '13' }, + { label: 'Smut', value: '29' }, + { label: 'Sports', value: '42' }, + { label: 'Supernatural', value: '18' }, + { label: 'System', value: '58' }, + { label: 'Tragedy', value: '32' }, + { label: 'Urban', value: '63' }, + { label: 'Urban Life', value: '59' }, + { label: 'Video Games', value: '60' }, + { label: 'War', value: '61' }, + { label: 'Wuxia', value: '31' }, + { label: 'Xianxia', value: '23' }, + { label: 'Xuanhuan', value: '22' }, + { label: 'Yaoi', value: '14' }, + { label: 'Yuri', value: '62' }, + ], + type: FilterTypes.CheckboxGroup, + }, + language: { + label: 'Language', + value: [], + options: [ + { label: 'Chinese', value: '1' }, + { label: 'Korean', value: '2' }, + { label: 'Japanese', value: '3' }, + { label: 'English', value: '4' }, + ], + type: FilterTypes.CheckboxGroup, + }, + rating_operator: { + label: 'Rating (Min/Max)', + value: 'min', + options: [ + { label: 'Min', value: 'min' }, + { label: 'Max', value: 'max' }, + ], + type: FilterTypes.Picker, + }, + rating: { + label: 'Rating', + value: '0', + options: [ + { label: 'All', value: '0' }, + { label: '1', value: '1' }, + { label: '2', value: '2' }, + { label: '3', value: '3' }, + { label: '4', value: '4' }, + { label: '5', value: '5' }, + ], + type: FilterTypes.Picker, + }, + chapters: { + label: 'Chapters', + value: '0', + options: [ + { label: 'All', value: '0' }, + { label: '<50', value: '1,49' }, + { label: '50-100', value: '50,100' }, + { label: '100-200', value: '100,200' }, + { label: '200-500', value: '200,500' }, + { label: '500-1000', value: '500,1000' }, + { label: '>1000', value: '1001,1000000' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new NovelFire(); + +// Custom error for when Novel Fire is rate limiting requests +class NovelFireThrottlingError extends Error { + constructor(message = 'Novel Fire is rate limiting requests') { + super(message); + this.name = 'NovelFireError'; + } +} + +class NovelFireAjaxNotFound extends Error { + constructor(message = 'Novel Fire says its Ajax interface is not found') { + super(message); + this.name = 'NovelFireAjaxError'; + } +} diff --git a/plugins/english/novelhall.ts b/plugins/english/novelhall.ts new file mode 100644 index 000000000..1d1a5cc33 --- /dev/null +++ b/plugins/english/novelhall.ts @@ -0,0 +1,123 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; + +class NovelHall implements Plugin.PluginBase { + id = 'novelhall'; + name = 'Novel Hall'; + version = '1.0.3'; + icon = 'src/en/novelhall/icon.png'; + site = 'https://novelhall.com/'; + + async popularNovels(page: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}all2022-${page}.html`; + + const body = await fetchApi(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('li.btm').each((idx, ele) => { + const novelName = loadedCheerio(ele).text().trim(); + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: defaultCover, + path: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.book-info > h1').text() || 'Untitled', + cover: loadedCheerio('meta[property="og:image"]').attr('content'), + summary: loadedCheerio('.intro').text().trim(), + chapters: [], + }; + + loadedCheerio('.total').find('p').remove(); + novel.author = loadedCheerio('.total span:contains("Author")') + .text() + .replace('Author:', '') + .trim(); + + novel.status = loadedCheerio('.total span:contains("Status")') + .text() + .replace('Status:', '') + .replace('Active', 'Ongoing') + .trim(); + + novel.genres = loadedCheerio('.total a') + .map((a, ex) => loadedCheerio(ex).text()) + .toArray() + .join(','); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('#morelist ul > li').each((idx, ele) => { + const chapterName = loadedCheerio(ele).find('a').text().trim(); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + }); + }); + + novel.chapters = chapter; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('#htmlContent').html() || ''; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}index.php?s=so&module=book&keyword=${encodeURIComponent(searchTerm)}`; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('table tr').each((idx, ele) => { + const novelName = loadedCheerio(ele) + .find('td:nth-child(2)') + .text() + .replace(/\t+/g, '') + .replace(/\n/g, ' '); + const novelUrl = loadedCheerio(ele) + .find('td:nth-child(2) a') + .attr('href'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: defaultCover, + path: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } +} + +export default new NovelHall(); diff --git a/plugins/english/novelhi.ts b/plugins/english/novelhi.ts new file mode 100644 index 000000000..4879e000f --- /dev/null +++ b/plugins/english/novelhi.ts @@ -0,0 +1,302 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class NovelHi implements Plugin.PluginBase { + id = 'novelhi'; + name = 'NovelHi'; + icon = 'src/en/novelhi/icon.png'; + site = 'https://novelhi.com/'; + version = '1.1.1'; + + // flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + // Cache for storing extended metadata from the list API | ie: copypasta from readfrom.ts + loadedNovelCache: CachedNovel[] = []; + + private async getNovels( + pageNo: number, + keyword: string | undefined, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + ): Promise<CachedNovel[]> { + const params = new URLSearchParams({ + curr: pageNo.toString(), + limit: '10', + ...(keyword && { keyword }), + ...(filters?.genres.value && { 'bookGenres[]': filters.genres.value }), + ...(filters?.order.value && { bookStatus: filters.order.value }), + ...(filters?.time.value && { updatePeriod: filters.time.value }), + }); + + const url = `${this.site}book/searchByPageInShelf?${params}`; + const response = await fetchApi(url); + const json: ApiResponse = await response.json(); + + const novels: CachedNovel[] = json.data.list.map(item => ({ + name: item.bookName, + path: `s/${item.simpleName}`, + cover: item.picUrl || defaultCover, + summary: item.bookDesc, + author: item.authorName, + status: item.bookStatus, + genres: item.genres.map(g => g.genreName).join(', '), + })); + + this.loadedNovelCache.push(...novels); + if (this.loadedNovelCache.length > 100) { + this.loadedNovelCache = this.loadedNovelCache.slice(-100); + } + + return novels; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<CachedNovel[]> { + return this.getNovels(pageNo, undefined, filters); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const data = await fetchApi(this.site + novelPath); + const text = await data.text(); + const loadedCheerio = parseHTML(text); + + const translate = loadedCheerio('#translate <').html(); + if (translate) { + console.error('This Novel has been removed and is no longer available'); + throw Error('This Novel has been removed and is no longer available'); + } + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: + loadedCheerio('b.layui-icon').text().trim() || + loadedCheerio('.tit h1').text().trim() || + 'Untitled', + cover: loadedCheerio('.cover,.decorate-img').attr('src') || defaultCover, + }; + + let moreNovelInfo = this.loadedNovelCache.find(n => n.path === novelPath); + + if (!moreNovelInfo) { + moreNovelInfo = (await this.searchNovels(novel.name, 1)).find( + novel => novel.path === novelPath, + ); + } + if (moreNovelInfo) { + novel.genres = moreNovelInfo.genres; + novel.author = moreNovelInfo.author; + novel.status = + moreNovelInfo.status === '1' + ? NovelStatus.Completed + : NovelStatus.Ongoing; + const summary = moreNovelInfo.summary.replace(/<br\s*\/?>/gi, '\n'); + novel.summary = parseHTML(summary).text().trim(); + } + + const chapters: Plugin.ChapterItem[] = []; + const bookId = loadedCheerio('#bookId').attr('value'); + if (bookId && !translate) { + const params = new URLSearchParams(); + params.append('bookId', bookId); + params.append('curr', '1'); + params.append('limit', '42121'); + + const url = `${this.site}book/queryIndexList?` + params.toString(); + const res = await fetchApi(url); + const resJson: ApiChapter = await res.json(); + + resJson?.data?.list?.forEach(chapter => + chapters.push({ + name: chapter.indexName, + path: novelPath + '/' + chapter.indexNum, + releaseTime: chapter.createTime, + }), + ); + } + + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + const result = await fetchApi(url).then(res => res.text()); + + const loadedCheerio = parseHTML(result); + const path = loadedCheerio('#chapterContentPath').attr('value'); + const token = loadedCheerio('#chapterContentToken').attr('value'); + + if (!path || !token) return ''; + + const contentPath = new URL(path, this.site).href; + const content: ApiContent = await fetchApi( + `${contentPath}?token=${token}`, + { + headers: { + 'Referer': url, + 'X-Requested-With': 'XMLHttpRequest', + }, + }, + ).then(r => r.json()); + + if (content) { + const rot13 = content.data.content.replace( + /(<[^>]+>)|([a-zA-Z])/g, + (_, tag, char) => { + if (tag) return tag; + + const base = char <= 'Z' ? 65 : 97; + const shift = ((char.charCodeAt(0) - base + 13) % 26) + base; + return String.fromCharCode(shift); + }, + ); + const chapter = rot13 + .replace(/<sent\b/gi, '<p') + .replace(/<\/sent>/gi, '</p>') + .replace(/<br\s*\/?>/gi, ''); + loadedCheerio('#showReading').html(chapter); + } + loadedCheerio('#showReading script,ins').remove(); + const chapterText = loadedCheerio('#showReading').html(); + if (!chapterText) { + return loadedCheerio('#translate').parent().html() || ''; + } + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<CachedNovel[]> { + return this.getNovels(pageNo, searchTerm); + } + + filters = { + genres: { + label: 'Genres', + value: '', + options: [ + { label: 'All', value: '' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Light Novel', value: 'light-novel' }, + { label: 'Fanfiction', value: 'fanfiction' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Game', value: 'game' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Military', value: 'military' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Urban Life', value: 'urban-life' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + order: { + label: 'Status', + value: '', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: '0' }, + { label: 'Completed', value: '1' }, + ], + type: FilterTypes.Picker, + }, + time: { + label: 'Update Period', + value: '', + options: [ + { label: 'All', value: '' }, + { label: '3 Days', value: '3' }, + { label: '7 Days', value: '7' }, + { label: '15 Days', value: '15' }, + { label: '30 Days', value: '30' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new NovelHi(); + +type CachedNovel = Plugin.NovelItem & { + summary: string; + genres: string; + author: string; + status: string; +}; + +type NovelData = { + id: string; + bookName: string; + picUrl: string; + simpleName: string; + authorName: string; + bookDesc: string; + bookStatus: string; + lastIndexName: string; + genres: { + genreId: string; + genreName: string; + }[]; +}; + +type ChapterData = { + id: string; + bookId: string; + indexNum: string; + indexName: string; + createTime: string; +}; + +type ApiResponse = { + code: string; + msg: string; + data: { + pageNum: string; + pageSize: string; + total: string; + list: NovelData[]; + }; +}; + +type ApiChapter = { + code: string; + msg: string; + data: { + pageNum: string; + pageSize: string; + total: string; + list: ChapterData[]; + }; +}; + +type ApiContent = { + code: number; + data: { + content: string; + }; +}; diff --git a/plugins/english/novelight.ts b/plugins/english/novelight.ts new file mode 100644 index 000000000..92e0211db --- /dev/null +++ b/plugins/english/novelight.ts @@ -0,0 +1,377 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import dayjs from 'dayjs'; +import { storage } from '@libs/storage'; + +class Novelight implements Plugin.PagePlugin { + id = 'novelight'; + name = 'Novelight'; + version = '1.1.5'; + icon = 'src/en/novelight/icon.png'; + site = 'https://novelight.net/'; + + hideLocked = storage.get('hideLocked'); + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = `${this.site}catalog/`; + if (showLatestNovels) { + url += `?ordering=-time_updated&page=${pageNo}`; + } else if (filters) { + const params = new URLSearchParams(); + for (const country of filters.country.value) { + params.append('country', country); + } + for (const genre of filters.genres.value) { + params.append('genre', genre); + } + for (const translation of filters.translation.value) { + params.append('translation', translation); + } + for (const status of filters.status.value) { + params.append('status', status); + } + for (const novel_type of filters.novel_type.value) { + params.append('type', novel_type); + } + params.append('ordering', filters.sort.value); + params.append('page', pageNo.toString()); + url += `?${params.toString()}`; + } else { + url += `?&ordering=popularity&page=${pageNo}`; + } + + const body = await fetchApi(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('a.item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('div.title').text().trim(); + const novelUrl = ele.attribs.href; + const bareNovelCover = loadedCheerio(ele).find('img').attr('src'); + const novelCover = bareNovelCover + ? this.site + bareNovelCover + : defaultCover; + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover ?? defaultCover, + path: novelUrl.replace('/', ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('h1').text() || 'Untitled', + cover: this.site + loadedCheerio('.poster > img').attr('src'), + summary: loadedCheerio('section.text-info.section > p').text(), + totalPages: loadedCheerio('#select-pagination-chapter > option').length, + chapters: [], + }; + + const info = loadedCheerio('div.mini-info > .item').toArray(); + let status = ''; + let translation = ''; + for (const child of info) { + const type = loadedCheerio(child).find('.sub-header').text().trim(); + if (type === 'Status') { + status = loadedCheerio(child) + .find('div.info') + .text() + .trim() + .toLowerCase(); + } + if (type === 'Translation') { + translation = loadedCheerio(child) + .find('div.info') + .text() + .trim() + .toLowerCase(); + } + if (type === 'Author') { + novel.author = loadedCheerio(child).find('div.info').text().trim(); + } + if (type === 'Genres') { + novel.genres = loadedCheerio(child) + .find('div.info > a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', '); + } + } + if (status === 'cancelled') novel.status = NovelStatus.Cancelled; + else if (status === 'releasing' || translation === 'ongoing') + novel.status = NovelStatus.Ongoing; + else if (status === 'completed' && translation === 'completed') + novel.status = NovelStatus.Completed; + + return novel; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const rawBody = await fetchApi(this.site + novelPath).then(r => r.text()); + const csrftoken = rawBody?.match(/window\.CSRF_TOKEN = "([^"]+)"/)?.[1]; + const bookId = rawBody?.match(/const OBJECT_BY_COMMENT = ([0-9]+)/)?.[1]; + const totalPages = parseInt( + rawBody + ?.match(/<option value="([0-9]+)"/g) + ?.at(-1) + ?.match(/([0-9]+)/)?.[1] ?? '1', + ); + + const r = await fetchApi( + `${this.site}book/ajax/chapter-pagination?csrfmiddlewaretoken=${csrftoken}&book_id=${bookId}&page=${totalPages - parseInt(page) + 1}`, + { + headers: { + 'Host': this.site.replace('https://', '').replace('/', ''), + 'Referer': this.site + novelPath, + 'X-Requested-With': 'XMLHttpRequest', + }, + }, + ); + + let chaptersRaw; + try { + chaptersRaw = await r.json(); + chaptersRaw = chaptersRaw.html; + } catch (error) { + console.error('Error Parsing Response'); + console.error(error); + throw new Error(error); + } + + const chapter: Plugin.ChapterItem[] = []; + + parseHTML('<html>' + chaptersRaw + '</html>')('a').each((idx, ele) => { + const title = parseHTML(ele)('.title').text().trim(); + const isLocked = !!parseHTML(ele)('.cost').text().trim(); + if (this.hideLocked && isLocked) return; + + let date; + try { + date = dayjs( + parseHTML(ele)('.date').text().trim(), + 'DD.MM.YYYY', + ).toISOString(); + } catch (error) { + // linter happy + } + + const chapterName = isLocked ? '🔒 ' + title : title; + let chapterUrl = ele.attribs.href; + if (chapterUrl.charAt(0) == '/') { + chapterUrl = chapterUrl.substring(1); + } + chapter.push({ + name: chapterName, + path: chapterUrl, + page: page, + releaseTime: date, + }); + }); + + const chapters = chapter.reverse(); + return { chapters }; + } + + async parseChapter(chapterPath: string): Promise<string> { + if (chapterPath.charAt(0) == '/') { + chapterPath = chapterPath.substring(1); + } + const rawBody = await fetchApi(this.site + chapterPath).then(r => { + const res = r.text(); + return res; + }); + + const csrftoken = rawBody?.match(/window\.CSRF_TOKEN = "([^"]+)"/)?.[1]; + const chapterId = rawBody?.match(/const CHAPTER_ID = "([0-9]+)/)?.[1]; + + let className; + const body = await fetchApi( + this.site + 'book/ajax/read-chapter/' + chapterId, + { + method: 'GET', + headers: { + Cookie: 'csrftoken=' + csrftoken, + Referer: this.site + chapterPath, + 'X-Requested-With': 'XMLHttpRequest', + }, + }, + ).then(async r => { + const res = await r.json(); + className = res.class; + return res.content; + }); + + const $ = parseHTML(body); + $('script').remove(); + $(`.${className} > *:not(br)`).after('<br>'); + const chapterText = $('.' + className).html() || ''; + + return chapterText.replace( + /class="advertisment"/g, + 'style="display:none;"', + ); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}catalog/?search=${encodeURIComponent(searchTerm)}`; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('a.item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('div.title').text().trim(); + const novelUrl = ele.attribs.href; + const bareNovelCover = loadedCheerio(ele).find('img').attr('src'); + const novelCover = bareNovelCover + ? this.site + bareNovelCover + : defaultCover; + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover ?? defaultCover, + path: novelUrl.replace('/', ''), + }; + + novels.push(novel); + }); + + return novels; + } + + filters = { + sort: { + label: 'Sort Results By', + value: 'popularity', + options: [ + { label: 'Title (A>Z)', value: 'title' }, + { label: 'Publication Date', value: '-time_created' }, + { label: 'Update Date (Newest)', value: '-time_updated' }, + { label: 'Year Release', value: '-year_of_release' }, + { label: 'Popularity', value: 'popularity' }, + ], + type: FilterTypes.Picker, + }, + translation: { + label: 'Translation Status', + value: [], + options: [ + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Paused', value: 'paused' }, + { label: 'Dropped', value: 'dropped' }, + { label: 'None', value: 'none' }, + ], + type: FilterTypes.CheckboxGroup, + }, + status: { + label: 'Status', + value: [], + options: [ + { label: 'Releasing', value: 'releasing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Cancelled', value: 'cancelled' }, + { label: 'Not yet released', value: 'not+yet+released' }, + ], + type: FilterTypes.CheckboxGroup, + }, + novel_type: { + label: 'Type', + value: [], + options: [ + { label: 'Fan Fiction', value: '4' }, + { label: 'Light Novel', value: '1' }, + { label: 'Published Novel', value: '2' }, + { label: 'Web Novel', value: '3' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + label: 'Genres', + value: [], + options: [ + { label: 'Thriller', value: '1' }, + { label: 'Supernatural', value: '2' }, + { label: 'Sports', value: '3' }, + { label: 'Slice of Life', value: '4' }, + { label: 'Sci-Fi', value: '5' }, + { label: 'Romance', value: '6' }, + { label: 'Psychological', value: '7' }, + { label: 'Mystery', value: '8' }, + { label: 'Mecha', value: '9' }, + { label: 'Horror', value: '10' }, + { label: 'Fantasy', value: '11' }, + { label: 'Ecchi', value: '12' }, + { label: 'Drama', value: '13' }, + { label: 'Comedy', value: '14' }, + { label: 'Adventure', value: '15' }, + { label: 'Action', value: '16' }, + { label: 'Adult', value: '17' }, + { label: 'Isekai', value: '18' }, + { label: 'Wuxia', value: '19' }, + { label: 'Shounen', value: '20' }, + { label: 'Yuri', value: '21' }, + { label: 'Shoujo', value: '22' }, + { label: 'Shoujo Ai', value: '23' }, + { label: 'Harem', value: '24' }, + { label: 'Seinen', value: '25' }, + { label: 'Tragedy', value: '26' }, + { label: 'Mature', value: '27' }, + { label: 'Martial Arts', value: '28' }, + { label: 'Gender Bender', value: '29' }, + { label: 'School Life', value: '30' }, + { label: 'Xuanhuan', value: '31' }, + { label: 'Yaoi', value: '32' }, + { label: 'Historical', value: '33' }, + ], + type: FilterTypes.CheckboxGroup, + }, + country: { + label: 'Country', + value: [], + options: [ + { label: 'China', value: '1' }, + { label: 'Japan', value: '2' }, + { label: 'Korea', value: '3' }, + { label: 'Other', value: '6' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new Novelight(); diff --git a/plugins/english/novelrest.ts b/plugins/english/novelrest.ts new file mode 100644 index 000000000..f7ac7af33 --- /dev/null +++ b/plugins/english/novelrest.ts @@ -0,0 +1,242 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +/** + * NovelRest LNReader Plugin + * + * This plugin allows LNReader to fetch novels from NovelRest (novelrest.vercel.app) + * + * Features: + * - Browse popular novels with pagination + * - Search novels + * - Read chapters + * - Filter by status + * - Sort by latest/popular/rating/updated + */ +class NovelRestPlugin implements Plugin.PluginBase { + id = 'novelrest'; + name = 'NovelRest'; + icon = 'src/en/novelrest/icon.png'; + site = 'https://novelrest.vercel.app'; + apiBase = 'https://novelrest.vercel.app/api/lnreader'; + version = '1.0.0'; + + filters: Filters = { + status: { + type: FilterTypes.Picker, + label: 'Status', + value: '', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'ONGOING' }, + { label: 'Completed', value: 'COMPLETED' }, + { label: 'Hiatus', value: 'HIATUS' }, + ], + }, + sort: { + type: FilterTypes.Picker, + label: 'Sort By', + value: 'latest', + options: [ + { label: 'Latest', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + { label: 'Rating', value: 'rating' }, + { label: 'Updated', value: 'updated' }, + ], + }, + }; + + /** + * Fetch popular/latest novels with pagination + */ + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + + // Build query parameters + const params = new URLSearchParams(); + params.set('page', pageNo.toString()); + params.set('limit', '20'); + + if (filters?.status?.value) { + params.set('status', filters.status.value as string); + } + + const sortBy = showLatestNovels + ? 'latest' + : (filters?.sort?.value as string) || 'popular'; + params.set('sort', sortBy); + + try { + const url = `${this.apiBase}/novels?${params.toString()}`; + const response = await fetchApi(url); + const data = await response.json(); + + if (data.novels && Array.isArray(data.novels)) { + for (const novel of data.novels) { + novels.push({ + name: novel.title, + path: novel.slug, // Just the slug, we'll build full path in parseNovel + cover: novel.coverImage || defaultCover, + }); + } + } + } catch (error) { + console.error('NovelRest: Error fetching popular novels:', error); + } + + return novels; + } + + /** + * Parse novel details and chapter list + */ + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + try { + // novelPath is just the slug + const slug = novelPath.replace(/^\/novels\//, ''); // Clean up if path prefix exists + + const response = await fetchApi(`${this.apiBase}/novels/${slug}`); + const data = await response.json(); + + if (data) { + novel.name = data.title || 'Untitled'; + novel.author = data.author || ''; + novel.cover = data.coverImage || defaultCover; + novel.genres = Array.isArray(data.genres) + ? data.genres + .map((g: { name: string } | string) => + typeof g === 'string' ? g : g.name, + ) + .join(', ') + : ''; + novel.summary = data.description || ''; + + // Map status + switch (data.status) { + case 'COMPLETED': + novel.status = NovelStatus.Completed; + break; + case 'ONGOING': + novel.status = NovelStatus.Ongoing; + break; + case 'HIATUS': + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + } + + // Parse chapters + const chapters: Plugin.ChapterItem[] = []; + + if (data.chapters && Array.isArray(data.chapters)) { + for (const chapter of data.chapters) { + chapters.push({ + name: chapter.title || `Chapter ${chapter.number}`, + path: `${slug}/${chapter.number}`, // slug/chapterNumber format + releaseTime: chapter.createdAt || undefined, + chapterNumber: chapter.number, + }); + } + } + + novel.chapters = chapters; + } + } catch (error) { + console.error('NovelRest: Error parsing novel:', error); + } + + return novel; + } + + /** + * Parse chapter content + */ + async parseChapter(chapterPath: string): Promise<string> { + try { + // chapterPath format: "slug/chapterNumber" + const parts = chapterPath.split('/'); + const chapterNumber = parts.pop(); + const slug = parts.join('/'); + + const response = await fetchApi( + `${this.apiBase}/novels/${slug}/chapters/${chapterNumber}`, + ); + const data = await response.json(); + + if (data && data.contentHtml) { + return data.contentHtml; + } + + return '<p>Chapter content could not be loaded.</p>'; + } catch (error) { + console.error('NovelRest: Error parsing chapter:', error); + return '<p>Error loading chapter content.</p>'; + } + } + + /** + * Search novels by term + */ + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + + try { + const params = new URLSearchParams(); + params.set('q', searchTerm); + params.set('page', pageNo.toString()); + params.set('limit', '20'); + + const response = await fetchApi( + `${this.apiBase}/novels?${params.toString()}`, + ); + const data = await response.json(); + + if (data.novels && Array.isArray(data.novels)) { + for (const novel of data.novels) { + novels.push({ + name: novel.title, + path: novel.slug, + cover: novel.coverImage || defaultCover, + }); + } + } + } catch (error) { + console.error('NovelRest: Error searching novels:', error); + } + + return novels; + } + + /** + * Resolve full URL for novel or chapter + */ + resolveUrl = (path: string, isNovel?: boolean): string => { + if (isNovel) { + return `${this.site}/novels/${path}`; + } + // For chapters, path is "slug/chapterNumber" + return `${this.site}/novels/${path}`; + }; +} + +export default new NovelRestPlugin(); + +// trigger build diff --git a/plugins/english/novelupdates.ts b/plugins/english/novelupdates.ts new file mode 100644 index 000000000..ae690a28d --- /dev/null +++ b/plugins/english/novelupdates.ts @@ -0,0 +1,1464 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; + +class NovelUpdates implements Plugin.PluginBase { + id = 'novelupdates'; + name = 'Novel Updates'; + version = '0.9.9'; + icon = 'src/en/novelupdates/icon.png'; + customCSS = 'src/en/novelupdates/customCSS.css'; + site = 'https://www.novelupdates.com/'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div.search_main_box_nu').each((_, el) => { + const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); + if (!novelUrl) return; + novels.push({ + name: loadedCheerio(el).find('.search_title > a').text(), + cover: loadedCheerio(el).find('img').attr('src'), + path: novelUrl.replace(this.site, ''), + }); + }); + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site; + + // Build the URL based on filters + if (showLatestNovels) { + url += 'series-finder/?sf=1&sort=sdate&order=desc'; + } else if ( + filters?.sort.value === 'popmonth' || + filters?.sort.value === 'popular' + ) { + url += 'series-ranking/?rank=' + filters.sort.value; + } else { + url += 'series-finder/?sf=1'; + if ( + filters?.genres.value.include?.length || + filters?.genres.value.exclude?.length + ) { + url += '&mgi=' + filters.genre_operator.value; + } + if (filters?.novelType.value.length) { + url += '&nt=' + filters.novelType.value.join(','); + } + if (filters?.reading_lists.value.length) { + url += '&hd=' + filters?.reading_lists.value.join(','); + url += '&mRLi=' + filters?.reading_list_operator.value; + } + url += '&sort=' + filters?.sort.value; + url += '&order=' + filters?.order.value; + } + + // Add common filters + if (filters?.language.value.length) + url += '&org=' + filters.language.value.join(','); + if (filters?.genres.value.include?.length) + url += '&gi=' + filters.genres.value.include.join(','); + if (filters?.genres.value.exclude?.length) + url += '&ge=' + filters.genres.value.exclude.join(','); + if (filters?.storyStatus.value) url += '&ss=' + filters.storyStatus.value; + + url += '&pg=' + page; + + const response = await fetchApi(url); + const body = await response.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + const response = await fetchApi(url); + const body = await response.text(); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.seriestitlenu').text() || 'Untitled', + cover: loadedCheerio('.wpb_wrapper img').attr('src'), + chapters: [], + }; + novel.author = loadedCheerio('#authtag') + .map((_, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(', '); + novel.genres = loadedCheerio('#seriesgenre') + .children('a') + .map((_, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + novel.status = loadedCheerio('#editstatus').text().includes('Ongoing') + ? 'Ongoing' + : 'Completed'; + + const type = loadedCheerio('#showtype').text().trim(); + const summary = loadedCheerio('#editdescription').text().trim(); + novel.summary = summary + `\n\nType: ${type}`; + const rating = loadedCheerio('.seriesother .uvotes') + .text() + .match(/(\d+\.\d+) \/ \d+\.\d+/)?.[1]; + if (rating) { + novel.rating = parseFloat(rating); + } + + const novelId = loadedCheerio('input#mypostid').attr('value')!; + const formData = new FormData(); + formData.append('action', 'nd_getchapters'); + formData.append('mygrr', '0'); + formData.append('mypostid', novelId); + + const chaptersHtml = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, { + method: 'POST', + body: formData, + }).then(data => data.text()); + + const chaptersCheerio = parseHTML(chaptersHtml); + const chapters: Plugin.ChapterItem[] = []; + + chaptersCheerio('li.sp_li_chp').each((_, el) => { + const chapterName = chaptersCheerio(el) + .text() + .replace('v', 'volume ') + .replace('c', ' chapter ') + .replace('part', 'part ') + .replace('ss', 'SS') + .replace(/\b\w/g, l => l.toUpperCase()) + .trim(); + + const chapterPath = + 'https:' + chaptersCheerio(el).find('a').first().next().attr('href'); + + if (chapterPath) + chapters.push({ + name: chapterName, + path: chapterPath.replace(this.site, ''), + }); + }); + + novel.chapters = chapters.reverse(); + return novel; + } + + getLocation(href: string) { + const match = href.match( + /^(https?:)\/\/(([^:/?#]*)(?::([0-9]+))?)([/]{0,1}[^?#]*)(\?[^#]*|)(#.*|)$/, + ); + return match && `${match[1]}//${match[3]}`; + } + + async getChapterBody( + loadedCheerio: CheerioAPI, + domain: string[], + chapterPath: string, + ) { + let bloatElements = []; + let chapterTitle = ''; + let chapterContent = ''; + let chapterText = ''; + + const unwanted = ['app', 'blogspot', 'casper', 'wordpress', 'www']; + const targetDomain = domain.find(d => !unwanted.includes(d)); + + switch (targetDomain) { + // Last edited in 0.9.4 by Batorian - 15/10/2025 + case 'akutranslations': { + try { + const apiUrl = chapterPath.replace('/novel', '/api/novel'); + const response = await fetchApi(apiUrl); + const json = await response.json(); + + if (!json?.content) { + throw new Error('Invalid API response structure.'); + } + + chapterContent = json.content + .trim() + .split(/\n+/) + .map((p: string) => p.trim()) + .filter((p: string) => p.length > 0) + .map((p: string) => `<p>${p}</p>`) + .join('\n'); + break; + } catch (error) { + throw new Error(`Failed to parse AkuTranslations chapter: ${error}`); + } + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'asuratls': { + const titleElement = loadedCheerio('.post-body div b').first(); + chapterTitle = titleElement.text(); + titleElement.remove(); + chapterContent = loadedCheerio('.post-body').html()!; + break; + } + // Last edited in 0.9.2 by Batorian - 08/09/2025 + case 'brightnovels': { + // Modular extraction inspired by W2e + const extractBrightNovelsContent = (cheerioInstance: CheerioAPI) => { + // Remove ad-related bloat elements + const bloatElements = ['.ad-container', 'script', 'style']; + bloatElements.forEach(tag => cheerioInstance(tag).remove()); + + // Extract the data-page attribute from <div id="app"> + const dataPage = cheerioInstance('#app').attr('data-page'); + if (!dataPage) { + throw new Error('data-page attribute not found on Bright Novels.'); + } + + // Parse the JSON from data-page + let pageData; + try { + pageData = JSON.parse(dataPage) as { + component: string; + props: { + chapter: { + id: number; + title: string; + content: string; + }; + }; + }; + } catch (e) { + throw new Error( + 'Failed to parse data-page JSON for Bright Novels.', + ); + } + + const chapterTitle = pageData.props.chapter.title; + let chapterContent = pageData.props.chapter.content; + + // Clean up content (remove inline styles/scripts if needed) + const chapterCheerio = parseHTML(chapterContent); + chapterCheerio('script, style').remove(); + chapterContent = chapterCheerio.html()!; + + // Return formatted HTML + return `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`; + }; + + try { + chapterText = extractBrightNovelsContent(loadedCheerio); + } catch (err) { + // Fallback: try to extract whatever is in #app or body + let fallbackContent = + loadedCheerio('#app').html() || loadedCheerio('body').html() || ''; + // Remove scripts/styles + const fallbackCheerio = parseHTML(fallbackContent); + fallbackCheerio('script, style').remove(); + fallbackContent = fallbackCheerio.html()!; + chapterText = fallbackContent; + } + break; + } + // Last edited in 0.9.2 by Batorian - 08/09/2025 + case 'canonstory': { + try { + const parts = chapterPath.split('/'); + if (parts.length < 7) { + throw new Error('Invalid chapter URL structure'); + } + + const novelSlug = parts[4]; + const chapterSlug = parts[6]; + const url = `${parts[0]}//${parts[2]}/api/public/chapter-by-slug/${novelSlug}/${chapterSlug}`; + + const response = await fetchApi(url); + const json = await response.json(); + if (!json?.data?.currentChapter) { + throw new Error('Invalid API response structure.'); + } + + const data = json.data.currentChapter; + const { chapterNumber, title, content } = data; + + const titleElement = `Chapter ${chapterNumber}`; + chapterTitle = title ? `${titleElement} - ${title}` : titleElement; + chapterContent = content.replace(/\n/g, '<br>'); + break; + } catch (error) { + throw new Error(`Failed to parse Canon Story chapter: ${error}`); + } + } + // Last edited in 0.9.3 by Batorian - 09/09/2025 + case 'daoist': { + chapterTitle = loadedCheerio('.chapter__title').first().text(); + + // Remove locked content indicators + loadedCheerio('span.patreon-lock-icon').remove(); + + // Handle lazy-loaded images + loadedCheerio('img[data-src]').each((_, el) => { + const $el = loadedCheerio(el); + const dataSrc = $el.attr('data-src'); + if (dataSrc) { + $el.attr('src', dataSrc); + $el.removeAttr('data-src'); + } + }); + + chapterContent = loadedCheerio('.chapter__content').html()!; + break; + } + // Last edited in 0.9.6 by Batorian - 16/01/2026 + case 'dreamy-translations': { + chapterTitle = loadedCheerio('h1 > span').first().text(); + + const content = loadedCheerio('.chapter-content > div').first(); + content.children('em').wrap('<p></p>'); + + chapterContent = content.html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'fictionread': { + bloatElements = [ + '.content > style', + '.highlight-ad-container', + '.meaning', + '.word', + ]; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('.title-image span').first().text(); + loadedCheerio('.content') + .children() + .each((_, el) => { + if (loadedCheerio(el).attr('id')?.includes('Chaptertitle-info')) { + loadedCheerio(el).remove(); + return false; + } + }); + chapterContent = loadedCheerio('.content').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025, + // remove any typing in 0.9.9 by K1ngfish3r - 04/05/2026, remove this comment if no issues + case 'genesistudio': { + const url = `${chapterPath}/__data.json?x-sveltekit-invalidated=001`; + try { + // Fetch the chapter's data in JSON format + const json = (await fetchApi(url).then(r => r.json())) as { + nodes: { type: string; data: Record<string, unknown> }[]; + }; + const nodes = json.nodes; + const data = nodes + .filter(node => node.type === 'data') + .map(node => node.data)[0]; + // Look for chapter container with required fields + const contentKey = 'content'; + const notesKey = 'notes'; + const footnotesKey = 'footnotes'; + // Iterate over each property in data to find chapter containers + for (const key in data) { + const mapping = data[key] as Record<string, number>; + // Check container for keys that match the required fields + if ( + mapping && + typeof mapping === 'object' && + contentKey in mapping && + notesKey in mapping && + footnotesKey in mapping + ) { + // Retrieve the chapter's content, notes, and footnotes using the mapping. + const content = data[String(mapping[contentKey])] as string; + const notes = data[String(mapping[notesKey])] as string; + const footnotes = data[String(mapping[footnotesKey])] as + | string + | undefined; + // Combine the parts with appropriate formatting + chapterText = + content + + (notes ? `<h2>Notes</h2><br>${notes}` : '') + + (footnotes ?? ''); + break; + } + } + } catch (error) { + throw new Error(`Failed to fetch chapter data: ${error}`); + } + break; + } + // Last edited in 0.9.6 by Batorian - 16/01/2026 + case 'greenz': { + const chapterSlug = chapterPath.split('/').pop(); + const apiUrl = `https://greenz.com/api/chapters/slug/${chapterSlug}`; + + try { + const response = await fetchApi(apiUrl); + const json = await response.json(); + + const chapterName = json.data.name; + const chapterNumber = json.data.chapterNumber; + const chapterCheerio = parseHTML(json.data.content); + + chapterTitle = `Chapter ${chapterNumber} - ${chapterName}`; + chapterContent = chapterCheerio.html()!; + } catch (error) { + throw new Error(`Failed to parse GreenzTL chapter: ${error}`); + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'hiraethtranslation': { + chapterTitle = loadedCheerio('li.active').first().text(); + chapterContent = loadedCheerio('.text-left').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'hostednovel': { + chapterTitle = loadedCheerio('#chapter-title').first().text(); + chapterContent = loadedCheerio('#chapter-content').html()!; + break; + } + // Last edited in 0.9.5 by Batorian - 26/12/2025 + case 'infinitenoveltranslations': { + // Get the chapter link from the main page + const url = loadedCheerio('article > p > a').first().attr('href')!; + if (url) { + const response = await fetchApi(url); + const body = await response.text(); + loadedCheerio = parseHTML(body); + } + chapterContent = loadedCheerio('.entry-content').html()!; + chapterTitle = loadedCheerio('.entry-title').text(); + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'inoveltranslation': { + bloatElements = ['header', 'section']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterText = loadedCheerio('.styles_content__JHK8G').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + // mii translates + case 'isotls': { + bloatElements = [ + 'footer', + 'header', + 'nav', + '.ezoic-ad', + '.ezoic-adpicker-ad', + '.ezoic-videopicker-video', + ]; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('head title').first().text(); + chapterContent = loadedCheerio('main article').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'ko-fi': { + const matchResult = loadedCheerio( + 'script:contains("shadowDom.innerHTML")', + ) + .html() + ?.match(/shadowDom\.innerHTML \+= '(<div.*?)';/); + if (matchResult && matchResult[1]) { + chapterText = matchResult[1]; + } + break; + } + // Last edited in 0.9.6 by Batorian - 16/01/2026 + case 'leafstudio': { + chapterTitle = loadedCheerio('.title').first().text(); + chapterContent = loadedCheerio('.chapter_content') + .map((_, el) => loadedCheerio(el).prop('outerHTML')) + .get() + .join(''); + break; + } + // Last edited in 0.9.2 by Batorian - 08/09/2025 + case 'machineslicedbread': { + const urlPath = chapterPath.split('/').filter(Boolean); + const pathSegments = urlPath.slice(2); + const pathDepth = pathSegments.length; + + let loadedCheerioSlicedBread = loadedCheerio; + + // Handle redirect pages + if (pathDepth === 1) { + const chapterPath = loadedCheerio('.entry-content a') + .first() + .attr('href'); + if (!chapterPath) { + throw new Error('Chapter path not found.'); + } + + const response = await fetchApi(chapterPath); + if (!response.ok) { + throw new Error(`Failed to fetch chapter: ${response.status}`); + } + + const body = await response.text(); + loadedCheerioSlicedBread = parseHTML(body); + } + + // Extract chapter content + chapterText = loadedCheerioSlicedBread('.entry-content').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'mirilu': { + bloatElements = ['#jp-post-flair']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + const titleElement = loadedCheerio('.entry-content p strong').first(); + chapterTitle = titleElement.text(); + titleElement.remove(); + chapterContent = loadedCheerio('.entry-content').html()!; + break; + } + // Last edited in 0.9.9 by Batorian - 09/05/2026 + case 'mythoriatales': { + /** + * Mythoria Tales uses Next.js Server Actions for chapter delivery. + * The response is a 'text/x-component' stream (RSC). + * + * Payload Structure: + * 0:{"a":"$@1",...} -> Initialization/Metadata + * 2:T{hexLen},... -> Chapter Body (may span multiple segments) + * 3:T{hexLen},... -> Chapter Body continuation (if split) + * 1:{"success":...} -> Series/Chapter JSON metadata (authoritative title source) + */ + const html = loadedCheerio('script:contains("script-2")').html(); + if (!html) throw new Error('Failed to find script-2'); + const matches = Array.from(html.matchAll(/"script-2.*?[^_]+([^\\]+)/g)); + const scriptPath = matches[1]?.[1]; + if (!scriptPath) throw new Error('Failed to extract script-2 URL'); + + const scriptUrl = new URL(`/${scriptPath}`, chapterPath).href; + const scriptText = await (await fetchApi(scriptUrl)).text(); + const ACTION_HASH = scriptText.match(/[a-f0-9]{42}/)?.[0]; + if (!ACTION_HASH) throw new Error('Failed to extract ACTION_HASH'); + + // chapterPath: https://www.mythoriatales.com/series/[slug]/chapter/[num] + const urlParts = chapterPath.split('/'); + const [slug, chapterNum] = [urlParts[4], parseInt(urlParts[6], 10)]; + + const response = await fetchApi(chapterPath, { + method: 'POST', + headers: { + 'Accept': 'text/x-component', + 'Content-Type': 'text/plain;charset=UTF-8', + 'next-action': ACTION_HASH, + }, + body: JSON.stringify([slug, chapterNum]), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch chapter: ${response.status}`); + } + + const rscText = (await response.text()).replace(/(\d+:[{TE])/g, '\n$1'); + + /** + * 1. Isolate the data segments. + * We split by newline followed by a digit and a type marker ({, T, E). + * Using a lookahead (?=...) ensures the split marker isn't consumed, + * allowing us to verify the segment index (e.g., "2:"). + */ + const segments = rscText.split(/\n(?=\d+:[{TE])/); + + /** + * 2. Locate and join all text content segments. + * Some chapters split their body across multiple T-type segments (e.g., 2:T, 3:T). + * We collect all of them (excluding the 0: init segment) and join into one string, + * stripping each segment's "{index}:T{hexLen}," prefix in the process. + */ + const contentSegment = segments + .filter(s => /^\d+:T/.test(s) && !s.startsWith('0:')) + .map(s => s.replace(/^\d+:T[0-9a-f]+,/, '')) + .join(''); + + if (!contentSegment) { + throw new Error( + 'Could not find the chapter content segment (2:T) in the stream.', + ); + } + + /** + * 3. Parse Lines and Paragraphs + * Splits on literal newlines or escaped sequence "\n". + * Filters out empty strings to handle double-spacing in the source. + */ + const lines = contentSegment + .trim() + .split(/(?:\r?\n|\\n)+/) + .map((line: string) => line.trim()) + .filter((line: string) => line.length > 0); + + if (lines.length === 0) { + throw new Error('Parsed content is empty.'); + } + + /** + * 4. Extract title from the "1:{...}" metadata segment. + * This is the authoritative source for the chapter title and number, + * preferred over parsing the first content line. + */ + const metaSegment = segments.find(s => s.startsWith('1:')); + if (metaSegment) { + try { + const meta = JSON.parse(metaSegment.slice(2)); // strip leading "1:" + const title = meta?.data?.chapter?.title; + const num = meta?.data?.chapter?.chapterNumber ?? chapterNum; + if (title) chapterTitle = `Chapter ${num}: ${title}`; + } catch { + // fall back to chapterNum if metadata parsing fails + } + } + if (!chapterTitle) chapterTitle = `Chapter ${chapterNum}`; + + // 5. All lines from the content segment are paragraphs. + chapterContent = lines.map((p: string) => `<p>${p}</p>`).join('\n'); + + // Clean up custom markup tags: + // Format [dialogue speaker="Name"]text[/dialogue] as "Name: text", drop [sfx] blocks entirely + chapterContent = chapterContent + .replace( + /\[dialogue\s+speaker="([^"]*)"\](.*?)\[\/dialogue\]/gi, + '$1: $2', + ) + .replace(/\[sfx\].*?\[\/sfx\]/gi, '') + .replace(/\[\/?(dialogue|sfx)[^\]]*\]/gi, ''); + + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'novelplex': { + bloatElements = ['.passingthrough_adreminder']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('.halChap--jud').first().text(); + chapterContent = loadedCheerio('.halChap--kontenInner ').html()!; + break; + } + case 'novelshub': { + const segments = chapterPath.split('/'); + const novelSlug = segments[segments.length - 2]; + const chapterSlug = segments[segments.length - 1]; + const apiUrl = `https://api.novelshub.org/api/chapter?mangaslug=${novelSlug}&chapterslug=${chapterSlug}`; + + try { + const response = await fetchApi(apiUrl); + const json = await response.json(); + + const chapterNumber = json.chapter.number; + const chapterCheerio = parseHTML(json.chapter.content); + + chapterTitle = `Chapter ${chapterNumber}`; + chapterCheerio('div').each((_, element) => { + const el = chapterCheerio(element); + const style = el.attr('style'); + if (!style) return; // Skip elements without inline styles + // Orange box + if (/border:.*#ff6b00/.test(style)) { + el.removeAttr('style').addClass('novels-hub_box_orange'); + // Orange box title + } else if ( + /color:.*#ff6b00.*text-transform:.*uppercase/.test(style) + ) { + el.removeAttr('style').addClass('novels-hub_box-title_orange'); + // Orange box text + } else if (/color:.*white.*border-top:.*#ff6b00/.test(style)) { + el.removeAttr('style').addClass('novels-hub_box-text_orange'); + // Green box + } else if (/border:.*#00ff88/.test(style)) { + el.removeAttr('style').addClass('novels-hub_box_green'); + // Green box title + } else if ( + /color:.*#00ff88.*text-transform:.*uppercase/.test(style) + ) { + el.removeAttr('style').addClass('novels-hub_box-title_green'); + // Green comment + } else if (/border-left:.*#00ff88/.test(style)) { + el.removeAttr('style').addClass('novels-hub_comment_green'); + // Blue box + } else if (/border:.*#0066ff/.test(style)) { + el.removeAttr('style').addClass('novels-hub_box_blue'); + // Blue box title + } else if ( + /color:.*#0099ff.*text-transform:.*uppercase/.test(style) + ) { + el.removeAttr('style').addClass('novels-hub_box-title_blue'); + // Blue box text + } else if (/color:.*#d0d0d0/.test(style)) { + el.removeAttr('style').addClass('novels-hub_box-text_blue'); + } + }); + chapterCheerio('span').each((_, element) => { + const el = chapterCheerio(element); + const style = el.attr('style'); + if (!style) return; // Skip elements without inline styles + // Red text + if (/color:.*#ff6b6b/.test(style)) { + el.removeAttr('style').addClass('novels-hub_text_red'); + // Blue text + } else if (/color:.*#4d9fff/.test(style)) { + el.removeAttr('style').addClass('novels-hub_text_blue'); + // Purple text + } else if (/color:.*#a78bfa/.test(style)) { + el.removeAttr('style').addClass('novels-hub_text_purple'); + } + }); + chapterContent = chapterCheerio.html()!; + } catch (error) { + throw new Error(`Failed to parse GreenzTL chapter: ${error}`); + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'novelworldtranslations': { + bloatElements = ['.separator img']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + loadedCheerio('.entry-content a') + .filter((_, el) => { + return ( + loadedCheerio(el) + .attr('href') + ?.includes('https://novelworldtranslations.blogspot.com') || + false + ); + }) + .each((_, el) => { + loadedCheerio(el).parent().remove(); + }); + chapterTitle = loadedCheerio('.entry-title').first().text(); + chapterContent = loadedCheerio('.entry-content') + .html()! + .replace(/ /g, '') + .replace(/\n/g, '<br>'); + // Load the chapter content into Cheerio and clean up empty elements + const chapterCheerio = parseHTML(chapterContent); + chapterCheerio('span, p, div').each((_, el) => { + if (chapterCheerio(el).text().trim() === '') { + chapterCheerio(el).remove(); + } + }); + chapterContent = chapterCheerio.html()!; + break; + } + // Last edited in 0.9.8 by K1ngfish3r - 04/04/2026 + // CF hyper aggressive or just NU shenanigans, login to patreon is 50/50 + case 'patreon': { + loadedCheerio('#track-click,[class*="hidden "]').remove(); + chapterTitle = loadedCheerio('h1[data-tag="post-title"]').text(); + chapterContent = loadedCheerio( + '[data-tag="post-card"] [class*="PaddingTop"]', + ).html()!; + break; + } + // Last edited in 0.9.7 by Batorian - 18/03/2026 + case 'r-p-d': { + let parts = chapterPath.split('/'); + + // 1. Resolve the redirect + const resolveRes = await fetchApi( + `${parts[0]}//${parts[2]}/resolve?p=/${parts.slice(3).join('/')}`, + ); + const { location } = await resolveRes.json(); + parts = location.split('/'); + const base = `${parts[0]}//${parts[2]}`; + + // 2. Get Meta & Token + const meta = await fetchApi( + `${base}/api/chapter-meta?seriesSlug=${parts[4]}&chapterSlug=${parts[5]}`, + ).then(r => r.json()); + const id = meta.chapter.id; + const { token } = await fetchApi( + `${base}/api/chapters/${id}/parts-token`, + ).then(r => r.json()); + + // 3. Fetch and Wrap in <p> tags + let total = 1; + for (let i = 1; i <= total; i++) { + const part = await fetchApi( + `${base}/api/chapters/${id}/parts?index=${i}&token=${token}`, + ).then(r => r.json()); + + // Wrap the entire part content + // We replace \n\n with </p><p> and wrap the edges in <p> and </p> + const formattedPart = + '<p>' + part.markdown.replace(/\n\n/g, '</p><p>') + '</p>'; + + chapterText += formattedPart; + total = part.total; + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'raeitranslations': { + const parts = chapterPath.split('/'); + const url = `${parts[0]}//api.${parts[2]}/api/chapters/single?id=${parts[3]}&num=${parts[4]}`; + const json = await fetchApi(url).then(r => r.json()); + const titleElement = `Chapter ${json.currentChapter.chapTag}`; + chapterTitle = json.currentChapter.chapTitle + ? `${titleElement} - ${json.currentChapter.chapTitle}` + : titleElement; + chapterContent = [ + json.novelHead, + `<br><hr><br>`, + json.currentChapter.body, + `<br><hr><br>Translator's Note:<br>`, + json.currentChapter.note, + ].join(''); + chapterContent = chapterContent.replace(/\n/g, '<br>'); + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'rainofsnow': { + const displayedDiv = loadedCheerio('.bb-item').filter(function () { + return loadedCheerio(this).css('display') === 'block'; + }); + const loadedCheerioSnow = parseHTML(displayedDiv.html()!); + bloatElements = [ + '.responsivevoice-button', + '.zoomdesc-cont p img', + '.zoomdesc-cont p noscript', + ]; + bloatElements.forEach(tag => loadedCheerioSnow(tag).remove()); + chapterContent = loadedCheerioSnow('.zoomdesc-cont').html()!; + const titleElement = loadedCheerioSnow('.scroller h2').first(); + if (titleElement.length) { + chapterTitle = titleElement.text(); + titleElement.remove(); + chapterContent = loadedCheerioSnow('.zoomdesc-cont').html()!; + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'readingpia': { + bloatElements = ['.ezoic-ad', '.ezoic-adpicker-ad', '.ez-video-wrap']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterText = loadedCheerio('.chapter-body').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'redoxtranslation': { + const chapterId = chapterPath.split('/').pop(); + chapterTitle = `Chapter ${chapterId}`; + const url = `${chapterPath.split('chapter')[0]}txt/${chapterId}.txt`; + chapterContent = await fetchApi(url) + .then(r => r.text()) + .then(text => { + // Split text into sentences based on newline characters + const sentences = text.split('\n'); + // Process each sentence individually + const formattedSentences = sentences.map(sentence => { + // Check if the sentence contains "<hr>" + if (sentence.includes('{break}')) { + // Create a centered sentence with three stars + return '<br> <p>****</p>'; + } else { + // Replace text enclosed within ** with <strong> tags + sentence = sentence.replace( + /\*\*(.*?)\*\*/g, + '<strong>$1</strong>', + ); + // Replace text enclosed within ++ with <em> tags + sentence = sentence.replace(/\+\+(.*?)\+\+/g, '<em>$1</em>'); + return sentence; + } + }); + // Join the formatted sentences back together with newline characters + return formattedSentences.join('<br>'); + }); + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'sacredtexttranslations': { + bloatElements = [ + '.entry-content blockquote', + '.entry-content div', + '.reaction-buttons', + ]; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('.entry-title').first().text(); + chapterContent = loadedCheerio('.entry-content').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'scribblehub': { + bloatElements = ['.wi_authornotes']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('.chapter-title').first().text(); + chapterContent = loadedCheerio('.chp_raw').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'skydemonorder': { + // Check for age verification + const ageVerification = loadedCheerio('main').text().toLowerCase()!; + if (ageVerification.includes('age verification required')) { + throw new Error('Age verification required, please open in webview.'); + } + chapterTitle = `${loadedCheerio('header .font-medium.text-sm').first().text().trim()}`; + chapterContent = loadedCheerio('#chapter-body').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'stabbingwithasyringe': { + // Get the chapter link from the main page + const url = loadedCheerio('.entry-content a').attr('href')!; + if (url) { + const response = await fetchApi(url); + const body = await response.text(); + loadedCheerio = parseHTML(body); + } + bloatElements = [ + '.has-inline-color', + '.wp-block-buttons', + '.wpcnt', + '#jp-post-flair', + ]; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterContent = loadedCheerio('.entry-content').html()!; + const titleElement = loadedCheerio('.entry-content h3').first(); + if (titleElement.length) { + chapterTitle = titleElement.text(); + titleElement.remove(); + chapterContent = loadedCheerio('.entry-content').html()!; + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'tinytranslation': { + bloatElements = [ + '.content noscript', + '.google_translate_element', + '.navigate', + '.post-views', + 'br', + ]; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('.title-content').first().text(); + loadedCheerio('.title-content').first().remove(); + chapterContent = loadedCheerio('.content').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'tumblr': { + chapterText = loadedCheerio('.post').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'vampiramtl': { + // Get the chapter link from the main page + const url = loadedCheerio('.entry-content a').attr('href')!; + if (url) { + const response = await fetchApi(chapterPath + url); + const body = await response.text(); + loadedCheerio = parseHTML(body); + } + chapterTitle = loadedCheerio('.entry-title').first().text(); + chapterContent = loadedCheerio('.entry-content').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'wattpad': { + chapterTitle = loadedCheerio('.h2').first().text(); + chapterContent = loadedCheerio('.part-content pre').html()!; + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'webnovel': { + chapterTitle = loadedCheerio('.cha-tit .pr .dib').first().text(); + chapterContent = loadedCheerio('.cha-words').html()!; + if (!chapterContent) { + chapterContent = loadedCheerio('._content').html()!; + } + break; + } + case 'wetriedtls': { + const scriptContent = + loadedCheerio('script:contains("p dir=")').html() || + loadedCheerio('script:contains("u003c")').html(); + if (scriptContent) { + const jsonString_wetried = scriptContent.slice( + scriptContent.indexOf('.push(') + '.push('.length, + scriptContent.lastIndexOf(')'), + ); + chapterText = JSON.parse(jsonString_wetried)[1]; + } + break; + } + // Last edited in 0.9.0 by Batorian - 19/03/2025 + case 'wuxiaworld': { + bloatElements = ['.MuiLink-root']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + chapterTitle = loadedCheerio('h4 span').first().text(); + chapterContent = loadedCheerio('.chapter-content').html()!; + break; + } + case 'yoru': { + const chapterId = chapterPath.split('/').pop(); + const url = `https://pxp-main-531j.onrender.com/api/v1/book_chapters/${chapterId}/content`; + const json = await fetchApi(url).then(r => r.json()); + chapterText = await fetchApi(json).then(r => r.text()); + break; + } + } + if (!chapterText) { + if (chapterTitle) { + chapterText = `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`; + } else { + chapterText = chapterContent; + } + } + return chapterText; + } + + async parseChapter(chapterPath: string): Promise<string> { + let chapterText; + + const response = await fetchApi(this.site + chapterPath); + const body = await response.text(); + const url = response.url; + const domainParts = url.toLowerCase().split('/')[2].split('.'); + + const loadedCheerio = parseHTML(body); + + // Handle CAPTCHA cases + const blockedTitles = [ + 'bot verification', + 'just a moment...', + 'redirecting...', + 'un instant...', + 'you are being redirected...', + ]; + const title = loadedCheerio('title').text().trim().toLowerCase(); + if (blockedTitles.includes(title)) { + throw new Error('Captcha detected, please open in webview.'); + } + + // Check if chapter url is wrong or site is down + if (!response.ok) { + throw new Error( + `Failed to fetch ${response.url}: ${response.status} ${response.statusText}`, + ); + } + + // Helper to safely check if any element matches a regex or string + const matches = (selector: string, attr: string | null, regex: RegExp) => { + let found = false; + loadedCheerio(selector).each((_, el) => { + const val = attr + ? loadedCheerio(el).attr(attr) + : loadedCheerio(el).html() || loadedCheerio(el).text(); + if (val && regex.test(val.toLowerCase())) { + found = true; + return false; + } + }); + return found; + }; + + // --- WordPress Detection --- + let isWordPress = [ + // 1. Meta Generator + matches('meta[name="generator"]', 'content', /wordpress|site kit/i), + // 2. Resource Paths (The most reliable way) + matches('link, script, img', 'src', /\/wp-content\/|\/wp-includes\//i), + matches('link', 'href', /\/wp-content\/|\/wp-includes\//i), + // 3. Header Links (REST API, RSD, etc.) + matches('link[rel="https://api.w.org/"]', 'href', /.*/), + matches('link[rel="EditURI"]', 'href', /xmlrpc\.php/i), + // 4. Common Body Classes + matches('body', 'class', /wp-admin|wp-custom-logo|logged-in/i), + // 5. Scripts containing WP globals + matches('script', null, /wp-embed|wp-emoji|wp-block/i), + ].some(Boolean); + + // --- Blogspot / Blogger Detection --- + let isBlogspot = [ + // 1. Meta Tags + matches('meta[name="generator"]', 'content', /blogger/i), + matches( + 'meta[name="google-adsense-platform-domain"]', + 'content', + /blogspot/i, + ), + // 2. Feed links + matches( + 'link[rel="alternate"]', + 'href', + /blogger\.com\/feeds|blogspot\.com\/feeds/i, + ), + // 3. Specific Blogger CSS/Template markers + matches( + 'link', + 'href', + /www\.blogger\.com\/static|www\.blogger\.com\/dyn-css/i, + ), + // 4. Blogger Script Widget Manager + matches( + 'script', + null, + /_WidgetManager\._Init|_WidgetManager\._RegisterWidget/i, + ), + ].some(Boolean); + + // Handle outlier sites + // Last edited in 0.9.6 - 16/01/2026 + const outliers = [ + 'asuratls', + 'fictionread', + 'hiraethtranslation', + 'infinitenoveltranslations', + 'leafstudio', + 'machineslicedbread', + 'mirilu', + 'novelworldtranslations', + 'sacredtexttranslations', + 'stabbingwithasyringe', + 'tinytranslation', + 'vampiramtl', + ]; + if (domainParts.some(d => outliers.includes(d))) { + isWordPress = false; + isBlogspot = false; + } + + // Last edited in 0.9.6 - 16/01/2026 + /** + * Blogspot sites: + * - ¼-Assed + * - AsuraTls (Outlier) + * - FictionRead (Outlier) + * - Novel World Translations (Outlier) + * - SacredText TL (Outlier) + * - Toasteful + * + * WordPress sites: + * - Dumah's Translations + * - Ether Reads + * - Femme Fables + * - Gadgetized Panda Translation + * - Goblinslate + * - Hiraeth Translation (Outlier) + * - Infinite Novel Translations (Outlier) + * - ippotranslations + * - JATranslations + * - Leaf Studio (Outlier) + * - Light Novels Translations + * - Machine Sliced Bread (Outlier) + * - Mirilu - Novel Reader Attempts Translating (Outlier) + * - Neosekai Translations + * - Noice Translations + * - Shanghai Fantasy + * - Sleepy Translations + * - Soafp + * - Stabbing with a Syringe (Outlier) + * - StoneScape + * - TinyTL (Outlier) + * - VampiraMTL (Outlier) + * - Wonder Novels + * - Yong Library + * - Zetro Translation + */ + + // Define Platform Configurations + const PLATFORM_CONFIG = { + wordpress: { + bloat: [ + '.ad', + '.author-avatar', + '.chapter-warning', + '.entry-meta', + '.ezoic-ad', + '.mb-center', + '.modern-footnotes-footnote__note', + '.patreon-widget', + '.post-cats', + '.pre-bar', + '.sharedaddy', + '.sidebar', + '.swg-button-v2-light', + '.wp-block-buttons', + '.wp-dark-mode-switcher', + '.wp-next-post-navi', + '#hpk', + '#jp-post-flair', + '#textbox', + ], + title: [ + '.entry-title', + '.chapter__title', + '.title-content', + '.wp-block-post-title', + '.title_story', + '#chapter-heading', + '.chapter-title', + 'head title', + 'h1:first-of-type', + 'h2:first-of-type', + '.active', + ], + content: [ + '.chapter__content', + '.entry-content', + '.text_story', + '.post-content', + '.contenta', + '.single_post', + '.main-content', + '.reader-content', + '#content', + '#the-content', + 'article.post', + '.chp_raw', + ], + }, + blogspot: { + bloat: ['.button-container', '.ChapterNav', '.ch-bottom', '.separator'], + title: ['.entry-title', '.post-title', 'head title'], + content: ['.content-post', '.entry-content', '.post-body'], + }, + }; + + // Extraction Logic + if (!isWordPress && !isBlogspot) { + chapterText = await this.getChapterBody(loadedCheerio, domainParts, url); + } else { + const config = isWordPress + ? PLATFORM_CONFIG.wordpress + : PLATFORM_CONFIG.blogspot; + + // Remove platform-specific bloat + config.bloat.forEach(tag => loadedCheerio(tag).remove()); + + // Extract Title (Simplified find) + let chapterTitle = config.title + .map(sel => loadedCheerio(sel).first().text().trim()) + .find(text => text.length > 0); + + // Handle Subtitles + const chapterSubtitle = + loadedCheerio('.cat-series').first().text() || + loadedCheerio('h1.leading-none ~ span').first().text() || + loadedCheerio('.breadcrumb .active').first().text(); + + if (chapterSubtitle) chapterTitle = chapterSubtitle; + + // Extract Content (Scoped search) + const chapterContent = config.content + .map(sel => { + const el = loadedCheerio(sel).first(); + // Ensure we don't pick up empty containers + return el.text().trim().length > 50 ? el.html() : null; + }) + .find(html => html); + + // Construct Final Text + if (chapterTitle && chapterContent) { + chapterText = `<h2>${chapterTitle}</h2><hr><br>${chapterContent}`; + } else { + chapterText = chapterContent || ''; + } + } + + // Fallback content extraction + if (!chapterText) { + ['nav', 'header', 'footer', '.hidden'].forEach(tag => + loadedCheerio(tag).remove(), + ); + chapterText = loadedCheerio('body').html()!; + } + + // Convert relative URLs to absolute + chapterText = chapterText.replace( + /href="\//g, + `href="${this.getLocation(response.url)}/`, + ); + + // Process images + const chapterCheerio = parseHTML(chapterText); + chapterCheerio('noscript').remove(); + + chapterCheerio('img').each((_, el) => { + const $el = chapterCheerio(el); + + // Only update if the lazy-loaded attribute exists + if ($el.attr('data-lazy-src')) { + $el.attr('src', $el.attr('data-lazy-src')); + } + if ($el.attr('data-lazy-srcset')) { + $el.attr('srcset', $el.attr('data-lazy-srcset')); + } + + // Remove lazy-loading class if it exists + if ($el.hasClass('lazyloaded')) { + $el.removeClass('lazyloaded'); + } + }); + + return chapterCheerio.html()!; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + // Split searchTerm by specific special characters and find the longest split + const splits = searchTerm.split('*'); + const longestSearchTerm = splits.reduce( + (a, b) => (a.length > b.length ? a : b), + '', + ); + searchTerm = longestSearchTerm.replace(/[‘’]/g, "'").replace(/\s+/g, '+'); + + const url = `${this.site}series-finder/?sf=1&sh=${searchTerm}&sort=srank&order=asc&pg=${page}`; + const response = await fetchApi(url); + const body = await response.text(); + + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + + filters = { + sort: { + label: 'Sort Results By', + value: 'popmonth', + options: [ + { label: 'Popular (Month)', value: 'popmonth' }, + { label: 'Popular (All)', value: 'popular' }, + { label: 'Last Updated', value: 'sdate' }, + { label: 'Rating', value: 'srate' }, + { label: 'Rank', value: 'srank' }, + { label: 'Reviews', value: 'sreview' }, + { label: 'Chapters', value: 'srel' }, + { label: 'Title', value: 'abc' }, + { label: 'Readers', value: 'sread' }, + { label: 'Frequency', value: 'sfrel' }, + ], + type: FilterTypes.Picker, + }, + order: { + label: 'Order (Not for Popular)', + value: 'desc', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + label: 'Story Status (Translation)', + value: '', + options: [ + { label: 'All', value: '' }, + { label: 'Completed', value: '2' }, + { label: 'Ongoing', value: '3' }, + { label: 'Hiatus', value: '4' }, + ], + type: FilterTypes.Picker, + }, + genre_operator: { + label: 'Genre (And/Or) (Not for Popular)', + value: 'and', + options: [ + { label: 'And', value: 'and' }, + { label: 'Or', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: '8' }, + { label: 'Adult', value: '280' }, + { label: 'Adventure', value: '13' }, + { label: 'Comedy', value: '17' }, + { label: 'Drama', value: '9' }, + { label: 'Ecchi', value: '292' }, + { label: 'Fantasy', value: '5' }, + { label: 'Gender Bender', value: '168' }, + { label: 'Harem', value: '3' }, + { label: 'Historical', value: '330' }, + { label: 'Horror', value: '343' }, + { label: 'Josei', value: '324' }, + { label: 'Martial Arts', value: '14' }, + { label: 'Mature', value: '4' }, + { label: 'Mecha', value: '10' }, + { label: 'Mystery', value: '245' }, + { label: 'Psychoical', value: '486' }, + { label: 'Romance', value: '15' }, + { label: 'School Life', value: '6' }, + { label: 'Sci-fi', value: '11' }, + { label: 'Seinen', value: '18' }, + { label: 'Shoujo', value: '157' }, + { label: 'Shoujo Ai', value: '851' }, + { label: 'Shounen', value: '12' }, + { label: 'Shounen Ai', value: '1692' }, + { label: 'Slice of Life', value: '7' }, + { label: 'Smut', value: '281' }, + { label: 'Sports', value: '1357' }, + { label: 'Supernatural', value: '16' }, + { label: 'Tragedy', value: '132' }, + { label: 'Wuxia', value: '479' }, + { label: 'Xianxia', value: '480' }, + { label: 'Xuanhuan', value: '3954' }, + { label: 'Yaoi', value: '560' }, + { label: 'Yuri', value: '922' }, + ], + }, + language: { + label: 'Language', + value: [], + options: [ + { label: 'Chinese', value: '495' }, + { label: 'Filipino', value: '9181' }, + { label: 'Indonesian', value: '9179' }, + { label: 'Japanese', value: '496' }, + { label: 'Khmer', value: '18657' }, + { label: 'Korean', value: '497' }, + { label: 'Malaysian', value: '9183' }, + { label: 'Thai', value: '9954' }, + { label: 'Vietnamese', value: '9177' }, + ], + type: FilterTypes.CheckboxGroup, + }, + novelType: { + label: 'Novel Type (Not for Popular)', + value: [], + options: [ + { label: 'Light Novel', value: '2443' }, + { label: 'Published Novel', value: '26874' }, + { label: 'Web Novel', value: '2444' }, + ], + type: FilterTypes.CheckboxGroup, + }, + reading_list_operator: { + label: 'Reading List (Include/Exclude) (Not for Popular)', + value: 'include', + options: [ + { label: 'Include', value: 'include' }, + { label: 'Exclude', value: 'exclude' }, + ], + type: FilterTypes.Picker, + }, + reading_lists: { + label: 'Reading Lists (Not for Popular)', + value: [], + options: [{ label: 'All Reading Lists', value: '-1' }], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new NovelUpdates(); diff --git a/plugins/english/pawread.ts b/plugins/english/pawread.ts new file mode 100644 index 000000000..09054ac6d --- /dev/null +++ b/plugins/english/pawread.ts @@ -0,0 +1,447 @@ +import { Parser } from 'htmlparser2'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class PawRead implements Plugin.PluginBase { + id = 'pawread'; + name = 'PawRead'; + version = '2.1.1'; + icon = 'src/en/pawread/icon.png'; + site = 'https://m.pawread.com/'; + + parseNovels(html: string) { + const novels: Plugin.NovelItem[] = []; + let tempNovel: Partial<Plugin.NovelItem> = {}; + let state: ParsingState = ParsingState.Idle; + const parser = new Parser({ + onopentag(name, attribs) { + if ( + attribs.class && + (attribs.class.includes('list-comic') || + attribs.class.includes('itemBox')) + ) { + state = ParsingState.Novel; + } + + if (state !== ParsingState.Novel) return; + + switch (name) { + case 'a': + if (attribs.class === 'txtA' || attribs.class === 'title') { + tempNovel.path = attribs.href.split('/').slice(1, 3).join('/'); + state = ParsingState.NovelName; + } + break; + case 'img': + tempNovel.cover = attribs.src; + break; + } + }, + + ontext(text) { + if (state === ParsingState.NovelName) { + tempNovel.name = (tempNovel.name || '') + text; + } + }, + + onclosetag(name) { + if (name === 'a') { + if (tempNovel.name && tempNovel.cover) { + novels.push(tempNovel as Plugin.NovelItem); + tempNovel = {}; + state = ParsingState.Idle; + } + } + }, + }); + + parser.write(html); + parser.end(); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = `${this.site}list/`; + + const filterValues = [ + filters.genre.value, + filters.status.value, + filters.lang.value, + ].filter(value => value !== ''); + + if (filterValues.length > 0) { + link += filterValues.join('-') + '/'; + } + + link += (filters.order.value ? '-' : '') + filters.sort.value; + link += `/?page=${page}`; + + const body = await fetchApi(link).then(r => r.text()); + return this.parseNovels(body); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const slash = novelPath.endsWith('/') ? '' : '/'; + const result = await fetchApi(this.site + novelPath + slash); + const body = await result.text(); + + const novel: Partial<Plugin.SourceNovel> = { + path: novelPath, + }; + + let depth = 0; + let state = ParsingState.Idle; + let tempChapter: Partial<Plugin.ChapterItem> = {}; + const chapter: Plugin.ChapterItem[] = []; + const summaryParts: string[] = []; + const genreArray: string[] = []; + + const parser = new Parser({ + onopentag(name, attribs) { + switch (name) { + case 'div': + if (attribs.id === 'Cover') { + state = ParsingState.Cover; + } + if (attribs.class?.includes('item-box')) { + state = ParsingState.Chapter; + const path = attribs.onclick.match(/\d+/)![0]; + tempChapter.path = `${novelPath}${slash}${path}.html`; + return; + } + if (state === ParsingState.Chapter) depth++; + break; + case 'img': + if (state === ParsingState.Cover) { + novel.name = attribs.title; + novel.cover = attribs.src; + } + break; + case 'p': + if (attribs.class === 'txtItme') { + if (!novel.status) { + state = ParsingState.Status; + } else if (!novel.author) { + state = ParsingState.Author; + } + } else if (attribs.id === 'full-des') { + state = ParsingState.Summary; + } + break; + case 'br': + summaryParts.push('\n'); + break; + case 'a': + if (attribs.class?.includes('btn-default')) { + state = ParsingState.Genres; + } + break; + case 'span': + if (state === ParsingState.Chapter) depth++; + if (depth === 2) { + state = ParsingState.ChapterName; + } else if (depth === 1) { + state = ParsingState.ChapterTime; + } + break; + } + }, + + ontext(text) { + switch (state) { + case ParsingState.Status: + novel.status = (novel.status || '') + text.trim(); + break; + case ParsingState.Author: + novel.author = (novel.author || '') + text.trim(); + break; + case ParsingState.Genres: + genreArray.push(text.trim()); + break; + case ParsingState.Summary: + summaryParts.push(text); + break; + case ParsingState.ChapterName: + tempChapter.name = (tempChapter.name || '') + text.trim(); + break; + case ParsingState.ChapterTime: + if (text?.includes('Advanced')) return; + { + const releaseDate = text.split('.').map(x => Number(x)); + if (releaseDate.length === 3) { + tempChapter.releaseTime = new Date( + releaseDate[0], + releaseDate[1] - 1, + releaseDate[2], + ).toISOString(); + } + } + break; + } + }, + + onclosetag(name) { + switch (name) { + case 'div': + if (state === ParsingState.Cover) { + state = ParsingState.Idle; + } + if (state === ParsingState.Chapter) { + depth--; + if (depth < 0) { + if ( + tempChapter.path && + tempChapter.name && + tempChapter.releaseTime + ) { + chapter.push(tempChapter as Plugin.ChapterItem); + } + tempChapter = {}; + depth = 0; + state = ParsingState.Idle; + } + } + break; + case 'p': + if ( + state === ParsingState.Status || + state === ParsingState.Author || + state === ParsingState.Summary + ) { + state = ParsingState.Idle; + } + break; + case 'a': + if (state === ParsingState.Genres) { + state = ParsingState.Idle; + } + break; + case 'span': + if ( + state === ParsingState.ChapterName || + state === ParsingState.ChapterTime + ) { + state = ParsingState.Chapter; + depth--; + } + break; + } + }, + + onend() { + novel.genres = genreArray.join(', '); + novel.summary = summaryParts.join(''); + novel.chapters = chapter; + }, + }); + + parser.write(body); + parser.end(); + + return novel as Plugin.SourceNovel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const html = await fetchApi(this.site + chapterPath).then(r => r.text()); + + let depth = 0; + let state: ParsingState = ParsingState.Idle; + const chapterHtml: string[] = []; + + const escapeMap: Record<string, string> = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + ' ': ' ', + }; + const escapeHtml = (text: string): string => + text.replace(/[&<>"'\xA0]/g, char => escapeMap[char]); + + const parser = new Parser({ + onopentag(name, attribs) { + switch (state) { + case ParsingState.Idle: + if (name === 'div' && attribs.class === 'main') { + state = ParsingState.Chapter; + depth++; + } + break; + case ParsingState.Chapter: + if (name === 'div') depth++; + break; + default: + return; + } + + if (state === ParsingState.Chapter) { + const attr = Object.keys(attribs).map(key => { + const value = attribs[key].replace(/"/g, '"'); + return ` ${key}="${value}"`; + }); + chapterHtml.push(`<${name}${attr.join('')}>`); + } + }, + + ontext(data) { + if (state === ParsingState.Chapter) { + const text = escapeHtml(data); + const icontains = ['pawread', 'tinyurl', 'bit.ly']; + + if (icontains.some(pattern => text?.includes(pattern))) { + state = ParsingState.Hidden; + chapterHtml.pop(); + } else { + chapterHtml.push(text); + } + } + }, + + onclosetag(name) { + switch (state) { + case ParsingState.Chapter: + if (!parser['isVoidElement'](name)) { + chapterHtml.push(`</${name}>`); + } + if (name === 'div') depth--; + if (depth === 0) { + state = ParsingState.Stopped; + } + break; + case ParsingState.Hidden: + state = ParsingState.Chapter; + break; + } + }, + }); + + parser.write(html); + parser.end(); + + return chapterHtml.join(''); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + keywords: searchTerm, + page: page.toString(), + }); + + const result = await fetchApi(`${this.site}search/?${params.toString()}`); + const body = await result.text(); + + return this.parseNovels(body); + } + + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Completed', value: 'wanjie' }, + { label: 'Ongoing', value: 'lianzai' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + }, + lang: { + value: '', + label: 'Languages', + options: [ + { label: 'All', value: '' }, + { label: 'Chinese', value: 'chinese' }, + { label: 'Korean', value: 'korean' }, + { label: 'Japanese', value: 'japanese' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Genres', + options: [ + { label: 'All', value: '' }, + { label: 'Fantasy', value: 'Fantasy' }, + { label: 'Action', value: 'Action' }, + { label: 'Xuanhuan', value: 'Xuanhuan' }, + { label: 'Romance', value: 'Romance' }, + { label: 'Comedy', value: 'Comedy' }, + { label: 'Mystery', value: 'Mystery' }, + { label: 'Mature', value: 'Mature' }, + { label: 'Harem', value: 'Harem' }, + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'Tragedy', value: 'Tragedy' }, + { label: 'Sci-fi', value: 'Scifi' }, + { label: 'Historical', value: 'Historical' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Adventure', value: 'Adventure' }, + { label: 'Adult', value: 'Adult' }, + { label: 'Supernatural', value: 'Supernatural' }, + { label: 'Psychological', value: 'Psychological' }, + { label: 'Drama', value: 'Drama' }, + { label: 'Horror', value: 'Horror' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Mecha', value: 'Mecha' }, + { label: 'Seinen', value: 'Seinen' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Shounen', value: 'Shounen' }, + { label: 'Smut', value: 'Smut' }, + { label: 'Yaoi', value: 'Yaoi' }, + { label: 'Yuri', value: 'Yuri' }, + { label: 'Martial Arts', value: 'MartialArts' }, + { label: 'School Life', value: 'SchoolLife' }, + { label: 'Shoujo Ai', value: 'ShoujoAi' }, + { label: 'Shounen Ai', value: 'ShounenAi' }, + { label: 'Slice of Life', value: 'SliceofLife' }, + { label: 'Gender Bender', value: 'GenderBender' }, + { label: 'Sports', value: 'Sports' }, + { label: 'Urban', value: 'Urban' }, + { label: 'Adventurer', value: 'Adventurer' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'click', + label: 'Sort By', + options: [ + { label: 'Time Updated', value: 'update' }, + { label: 'Time Posted', value: 'post' }, + { label: 'Clicks', value: 'click' }, + ], + type: FilterTypes.Picker, + }, + order: { + value: false, + label: 'Order ↑ ↓', + type: FilterTypes.Switch, + }, + } satisfies Filters; +} + +export default new PawRead(); + +enum ParsingState { + Idle, + Cover, + Genres, + Author, + Status, + Hidden, + Summary, + Stopped, + Chapter, + ChapterName, + ChapterTime, + NovelName, + Novel, +} diff --git a/plugins/english/rainofsnow.ts b/plugins/english/rainofsnow.ts new file mode 100644 index 000000000..255c7a7c4 --- /dev/null +++ b/plugins/english/rainofsnow.ts @@ -0,0 +1,195 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; + +class Rainofsnow implements Plugin.PagePlugin { + id = 'rainofsnow'; + name = 'Rainofsnow'; + icon = 'src/en/rainofsnow/icon.png'; + site = 'https://rainofsnow.com/'; + version = '1.1.2'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.minbox').each((index, element) => { + const name = loadedCheerio(element).find('h3').text(); + const cover = loadedCheerio(element).find('img').attr('data-src'); + const path = loadedCheerio(element) + .find('h3 > a') + .attr('href') + ?.replace(this.site, '') + .replace(/\/+$/, ''); + + if (!path) { + return; + } + + novels.push({ name, cover, path }); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const url = this.site + 'novels/page/' + pageNo + filters.genre.value; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: '', + totalPages: 0, + }; + + const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + novel.name = loadedCheerio('.text h2').text().trim(); + + novel.cover = loadedCheerio('.imagboca1 img').attr('data-src'); + + novel.summary = loadedCheerio('#synop').text().trim(); + + novel.genres = loadedCheerio('span:contains("Genre(s)")') + .next() + .text() + .trim(); + + novel.author = loadedCheerio('span:contains("Author")').next().text(); + + let x = 1; + loadedCheerio('.page-numbers li').each((i, el) => { + const num = loadedCheerio(el).find('a').text().trim().match(/(\d+)/); + const n = Number(num?.[1] || '0'); + if (n > x) { + x = n; + } + }); + + novel.totalPages = x; + novel.chapters = this.parseChapters(loadedCheerio); + novel.status = NovelStatus.Unknown; + return novel; + } + + // parse paged chapters + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = this.site + novelPath + '/page/' + page + '/#chapter'; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { chapters }; + } + + // helper to parse a novel + parseChapters(loadedCheerio: CheerioAPI) { + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('#chapter .march1 li').each((i, el) => { + const path = loadedCheerio(el) + .find('a') + .attr('href') + ?.slice(this.site.length); + if (!path) return; + const name = loadedCheerio(el).find('.chapter').first().text().trim(); + const date = loadedCheerio(el).find('small').text().trim().toLowerCase(); + const months = [ + 'jan', + 'feb', + 'mar', + 'apr', + 'may', + 'jun', + 'jul', + 'aug', + 'sep', + 'oct', + 'nov', + 'dec', + ]; + const regex = /([a-z]+) (..?), (.+)/; + const [, monthName, day, year] = regex.exec(date) || []; + const month = months.indexOf(monthName.slice(0, 3)); + const releaseTime = + monthName && month !== -1 + ? new Date(+year, month, +day).toISOString() + : null; + + chapter.push({ + name, + path, + releaseTime, + }); + }); + + return chapter; + } + + async parseChapter(chapterPath: string): Promise<string> { + // parse chapter text here + const result = await fetch(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterName = loadedCheerio('.content > h2').text(); + const chapterText = loadedCheerio('.content').html(); + if (!chapterText) return ''; + return chapterName + '\n' + chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + // get novels using the search term + // no page number, infinite scroll + + const newSearch = searchTerm.replace(/\s+/g, '+'); + const url = this.site + '?s=' + encodeURIComponent(newSearch); + + const result = await fetch(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + genre: { + value: '', + label: 'Filter By', + options: [ + { label: 'All', value: '' }, + { label: 'Action', value: '?n_orderby=16' }, + { label: 'Adventure', value: '?n_orderby=11' }, + { label: 'Angst', value: '?n_orderby=776' }, + { label: 'Chinese', value: '?n_orderby=342' }, + { label: 'Comedy', value: '?n_orderby=13' }, + { label: 'Drama', value: '?n_orderby=3' }, + { label: 'Fantasy', value: '?n_orderby=7' }, + { label: 'Japanese', value: '?n_orderby=343' }, + { label: 'Korean', value: '?n_orderby=341' }, + { label: 'Mature', value: '?n_orderby=778' }, + { label: 'Mystery', value: '?n_orderby=12' }, + { label: 'Original Novel', value: '?n_orderby=339' }, + { label: 'Psychological', value: '?n_orderby=769' }, + { label: 'Romance', value: '?n_orderby=5' }, + { label: 'Sci-fi', value: '?n_orderby=14' }, + { label: 'Slice of Life', value: '?n_orderby=779' }, + { label: 'Supernatural', value: '?n_orderby=780' }, + { label: 'Tragedy', value: '?n_orderby=777' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new Rainofsnow(); diff --git a/plugins/english/readfrom.ts b/plugins/english/readfrom.ts new file mode 100644 index 000000000..d1721ee73 --- /dev/null +++ b/plugins/english/readfrom.ts @@ -0,0 +1,218 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { CheerioAPI, load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; + +const pluginHeaders = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36', +}; + +class ReadFromPlugin implements Plugin.PluginBase { + id = 'readfrom'; + name = 'Read From Net'; + icon = 'src/en/readfrom/icon.png'; + site = 'https://readfrom.net/'; + version = '1.1.0'; + filters: Filters | undefined = undefined; + headers = new Headers(pluginHeaders); + imageRequestInit: Plugin.ImageRequestInit = { + headers: pluginHeaders, + }; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + loadedNovelCache: (Plugin.NovelItem & { + summary: string; + genres: string; + author: string; + })[] = []; + + parseNovels( + loadedCheerio: CheerioAPI, + isSearch?: boolean, + ): (Plugin.NovelItem & { + summary: string; + genres: string; + author: string; + })[] { + const ret = loadedCheerio( + (isSearch ? 'div.text' : '#dle-content') + ' > article.box', + ) + .map((i, el) => { + const $el = loadedCheerio(el); + const novelPath = $el.find('h2.title a').attr('href'); + if (!novelPath) return; + const summary = loadedCheerio(el).find( + isSearch ? 'div.text5' : 'div.text3', + )[0]; + loadedCheerio(summary).find('.coll-ellipsis').remove(); + loadedCheerio(summary).find('a').remove(); + return { + name: loadedCheerio(el).find('h2.title').text().trim(), + path: new URL(novelPath, this.site).pathname.substring(1), + cover: loadedCheerio(el).find('img').attr('src') || defaultCover, + summary: + loadedCheerio(summary).text().trim() + + loadedCheerio(summary).find('span.coll-hidden').text(), + genres: loadedCheerio(el) + .find(isSearch ? 'h5.title > a' : 'h2 > a') + .filter((i, el) => el.attribs['title']?.startsWith?.('Genre - ')) + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', '), + author: isSearch + ? loadedCheerio(el) + .find('h5.title > a') + .filter((i, el) => + el.attribs['title']?.startsWith?.('Book author - '), + ) + .text() + : loadedCheerio(el).find('h4 > a').text(), + }; + }) + .toArray(); + + this.loadedNovelCache.push(...ret); + while (this.loadedNovelCache.length > 100) { + this.loadedNovelCache.shift(); + } + + return ret; + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + const type = showLatestNovels ? 'last_added_books' : 'allbooks'; + const res = await fetchApi(this.site + type + '/page/' + pageNo, { + headers: this.headers, + }); + const text = await res.text(); + return this.parseNovels(loadCheerio(text)); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const data = await fetchApi(this.site + novelPath, { + headers: this.headers, + }); + const text = await data.text(); + const loadedCheerio = loadCheerio(text); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + novel.name = loadedCheerio('center > h2.title') + .text() + .split(', \n\n')[0] + .trim(); + novel.cover = + loadedCheerio('article.box > div > center > div > a > img').attr('src') || + defaultCover; + + const rawChapters = loadedCheerio('div.pages') + .first() + .find('> a') + .toArray() + .flatMap(el => { + const href = loadedCheerio(el).attr('href'); + if (!href) return []; + + const path = new URL(href, this.site).pathname.substring(1); + return path ? [{ name: loadedCheerio(el).text().trim(), path }] : []; + }); + + novel.chapters = [ + { name: '1', path: novelPath, chapterNumber: 1 }, + ...rawChapters.map((ch, i) => ({ ...ch, chapterNumber: i + 2 })), + ]; + + let moreNovelInfo = this.loadedNovelCache.find( + novel => novel.path === novelPath, + ); + if (!moreNovelInfo) + moreNovelInfo = (await this.searchNovels(novel.name, 1)).find( + novel => novel.path === novelPath, + ); + if (moreNovelInfo) { + novel.summary = moreNovelInfo.summary; + novel.genres = moreNovelInfo.genres; + novel.author = moreNovelInfo.author; + } + + const seriesElm = loadedCheerio('center > b:has(a)').filter((i, el) => + loadedCheerio(el).find('a').attr('href')!.startsWith('/series.html'), + )[0]; + + if (seriesElm) { + const seriesText = loadedCheerio(seriesElm).text().trim(); + + novel.summary = seriesText + '\n\n' + novel.summary; + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const response = await fetchApi(this.site + chapterPath, { + headers: this.headers, + }); + const $ = loadCheerio(await response.text()); + $('#textToRead > span:empty, #textToRead > center').remove(); + + const chapterHtml: string[] = []; + let p: string[] = []; + + const allowed = new Set(['b', 'i', 'u', 'strong', 'em', 'a']); + const flush = () => + p.length && (chapterHtml.push(`<p>${p.join(' ').trim()}</p>`), (p = [])); + + for (const el of $('#textToRead').contents().toArray()) { + switch (el.type) { + case 'comment': + continue; + case 'text': + if (el.data.trim()) { + // Convert _text_ to <i>text</i> + const jbText = el.data.trim().replace(/_([^_]+)_/g, '<i>$1</i>'); + p.push(jbText); + } + continue; + case 'tag': + if (allowed.has(el.name)) { + p.push($.html(el)); + continue; + } + if (el.name === 'br') { + flush(); + continue; + } + } + flush(); + chapterHtml.push($.html(el)); + } + + flush(); + return chapterHtml.join(''); + } + + async searchNovels(searchTerm: string, pageNo: number) { + if (pageNo !== 1) return []; + const res = await fetchApi( + 'https://readfrom.net/build_in_search/?q=' + + encodeURIComponent(searchTerm), + { headers: this.headers }, + ); + const text = await res.text(); + return this.parseNovels(loadCheerio(text), true); + } + + // resolveUrl = (path: string, isNovel?: boolean) => this.site + '/' + path; +} + +export default new ReadFromPlugin(); diff --git a/plugins/english/readlitenovel.broken.ts b/plugins/english/readlitenovel.broken.ts new file mode 100644 index 000000000..546d03d3e --- /dev/null +++ b/plugins/english/readlitenovel.broken.ts @@ -0,0 +1,190 @@ +import { load as parseHTML } from 'cheerio'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; + +class ReadLiteNovel implements Plugin.PluginBase { + id = 'rln.app'; + name = 'ReadLiteNovel'; + version = '1.0.1'; + icon = 'src/en/readlitenovel/icon.png'; + site = 'https://rln.app'; + + parseAgoDate(date: string | undefined) { + //parseMadaraDate + if (date?.includes('ago')) { + const dayJSDate = dayjs(new Date()); // today + const timeAgo = date.match(/\d+/)?.[0] || ''; + const timeAgoInt = parseInt(timeAgo, 10); + + if (!timeAgo) return null; // there is no number! + + if (date.includes('hours ago') || date.includes('hour ago')) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } + + if (date.includes('days ago') || date.includes('day ago')) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } + + if (date.includes('months ago') || date.includes('month ago')) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } + + if (date.includes('years ago') || date.includes('year ago')) { + dayJSDate.subtract(timeAgoInt, 'years'); // go back N years + } + + return dayJSDate.toISOString(); + } + return null; // there is no "ago" so give up + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const link = `${this.site}/ranking/${filters.order.value}/${page}`; + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.category-items li').each((i, el) => { + const novelUrl = loadedCheerio(el).find('.category-name a').attr('href'); + + if (!novelUrl) return; + const novelName = loadedCheerio(el) + .find('.category-name a') + .text() + .trim(); + let novelCover = loadedCheerio(el).find('.category-img img').attr('src'); + + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = this.site + novelCover; + } + + const novel = { + path: novelUrl?.replace(this.site, ''), + name: novelName, + cover: novelCover, + }; + novels.push(novel); + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.novel-title').text() || 'Untitled', + cover: loadedCheerio('.novels-detail img').attr('src'), + summary: loadedCheerio('.empty-box').text().trim(), + chapters: [], + }; + + loadedCheerio('.novels-detail-right li').each((i, el) => { + const detailName = loadedCheerio(el).find('div:first').text(); + const detail = loadedCheerio(el).find('div:last'); + + switch (detailName) { + case 'Status:': + novel.status = detail.text(); + break; + case 'Genres:': + novel.genres = + detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case 'Author(s):': + novel.author = + detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(', ') || detail.text(); + break; + case 'Translator:': + novel.artist = detail.text(); + break; + } + }); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.cm-tabs-content li').each((i, el) => { + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) return; + const chapterName = loadedCheerio(el).find('a').text().trim(); + const releaseDate = this.parseAgoDate( + loadedCheerio(el).find('svg').attr('data-bs-original-title')!, + ); + + chapter.push({ + name: chapterName, + path: chapterUrl?.replace(this.site, ''), + releaseTime: releaseDate, + }); + }); + + novel.chapters = chapter; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chapterText').html() || ''; + + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = + this.site + + `/search/autocomplete?dataType=json&query=${encodeURIComponent(searchTerm)}`; + const result = await fetchApi(url); + const body = await result.json(); + const novels: Plugin.NovelItem[] = []; + + body.results.forEach( + (item: { link: string; original_title: string; image: string }) => + novels.push({ + path: item.link, + name: item.original_title, + cover: item.image, + }), + ); + + return novels; + } + + filters = { + order: { + value: 'top-rated', + label: 'Order by', + options: [ + { label: 'MOST VIEWED', value: 'most-viewed' }, + { label: 'TOP RATED', value: 'top-rated' }, + { label: 'BOOKMARKS', value: 'subscribers' }, + { label: 'NEW', value: 'new' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new ReadLiteNovel(); diff --git a/plugins/english/reaperscans.broken.ts b/plugins/english/reaperscans.broken.ts new file mode 100644 index 000000000..c7a6ae25b --- /dev/null +++ b/plugins/english/reaperscans.broken.ts @@ -0,0 +1,128 @@ +import { fetchApi } from '@libs/fetch'; +import type { Plugin } from '@/types/plugin'; + +const API_BASE = 'https://api.reaperscans.com'; +const MEDIA_BASE = 'https://media.reaperscans.com/file/4SRBHm/'; + +type ApiResponse<T> = { + data: T; + meta: { + total: number; + per_page: number; + current_page: number; + last_page: number; + }; +}; + +type ReaperNovel = { + title: string; + thumbnail: string; + series_slug: string; +}; + +class ReaperScans implements Plugin.PluginBase { + id = 'reaperscans.com'; + name = 'Reaper Scans'; + version = '1.0.0'; + icon = 'src/en/reaperscans/icon.png'; + site = 'https://reaperscans.com'; + + async popularNovels(page: number): Promise<Plugin.NovelItem[]> { + return this.query(page); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novelResp = await fetchApi(`${API_BASE}/series/${novelPath}`); + const novel: { + title: string; + thumbnail: string; + description: string; + tags: string[]; + rating: number; + status: string; + alternative_names: string; + author: string; + studio: string; + } = await novelResp.json(); + + const chaptersResp = await fetchApi( + `${API_BASE}/chapters/${novelPath}?perPage=${Number.MAX_SAFE_INTEGER}`, + ); + const chapters: { + chapter_slug: string; + chapter_name: string; + index: string; + created_at: string; + }[] = (await chaptersResp.json()).data; + + return { + name: novel.title, + cover: this.getCoverUrl(novel.thumbnail), + author: novel.author, + artist: novel.studio, + status: novel.status, + rating: novel.rating, + summary: novel.description, + genres: novel.tags.join(','), + path: novelPath, + chapters: chapters.reverse().map(chapter => ({ + path: `${novelPath}/${chapter.chapter_slug}`, + name: chapter.chapter_name, + chapterNumber: Number.parseFloat(chapter.index), + releaseTime: chapter.created_at.substring(0, 10), + })), + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(`${this.site}/series/${chapterPath}`, { + headers: { RSC: '1' }, + }); + const body = await result.text(); + return this.extractChapterContent(body); + } + + private extractChapterContent(chapter: string): string { + const lines = chapter.split('\n'); + const start = lines.findIndex(line => line.includes('<p')); + + const prefix = lines[start].substring(0, lines[start].indexOf('<')); + const commonPrefix = prefix.substring( + prefix.indexOf(':'), + prefix.indexOf(','), + ); + + const end = lines.lastIndexOf(commonPrefix); + const content = lines.slice(start, end).join('\n'); + + const deduplicated = content.split(commonPrefix)[1]; + return deduplicated.substring( + deduplicated.indexOf('<'), + deduplicated.lastIndexOf('>') + 1, + ); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + return this.query(1, searchTerm); + } + + private async query(page = 1, search = ''): Promise<Plugin.NovelItem[]> { + const link = `${API_BASE}/query?page=${page}&perPage=20&series_type=Novel&query_string=${search}&order=desc&orderBy=created_at&adult=true&status=All&tags_ids=[]`; + const result = await fetchApi(link); + const json: ApiResponse<ReaperNovel[]> = await result.json(); + + return json.data.map(novel => ({ + name: novel.title, + cover: novel.thumbnail.startsWith('novels/') + ? MEDIA_BASE + novel.thumbnail + : novel.thumbnail, + path: novel.series_slug, + })); + } + + private getCoverUrl(thumbnail: string): string { + return thumbnail.startsWith('novels/') ? MEDIA_BASE + thumbnail : thumbnail; + } +} + +export default new ReaperScans(); diff --git a/plugins/english/relibrary.ts b/plugins/english/relibrary.ts new file mode 100644 index 000000000..8cbd1844b --- /dev/null +++ b/plugins/english/relibrary.ts @@ -0,0 +1,416 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +type FuzzySearchOptions = { + caseSensitive: boolean; + sort: boolean; +}; + +// Shamelessly stolen from 'https://raw.githubusercontent.com/wouterrutgers/fuzzy-search/master/src/FuzzySearch.mjs' +// I did modify the code a fair bit, but the algorithm is still the same under the hood. +// The original code is under ISC, which stipluate that it needs to retain the copyright notice, so here it is. +// +// It only applies to the code in the FuzzySearch Class, even though I did modify it a fair bit + +/* +Copyright (c) 2016, Wouter Rutgers + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +class FuzzySearch<Item> { + private haystack: Item[]; + private options: FuzzySearchOptions; + private getItems: (item: Item) => string[]; + + constructor( + getItems: (item: Item) => string[], + options: Partial<FuzzySearchOptions>, + ) { + this.haystack = []; + this.options = Object.assign( + { + caseSensitive: false, + sort: false, + }, + options, + ); + this.getItems = getItems; + } + + getOptions(): FuzzySearchOptions { + return this.options; + } + + setOptions(options: Partial<FuzzySearchOptions>) { + this.options = Object.assign(this.options, options); + } + + setHaystack(items: Item[]) { + this.haystack = items; + } + + getHaystack(): Item[] { + return this.haystack; + } + + search(query: string): Item[] { + if (query.length === 0) { + return this.haystack; + } + const results: { item: Item; score: number }[] = []; + + for (const item of this.haystack) { + for (const value of this.getItems(item)) { + const score = this.isMatch(value, query); + if (score !== undefined) { + results.push({ item, score }); + /// We don't want to have multiple occurence of the same item, so only take one if it is found + break; + } + } + } + + if (this.options.sort) { + results.sort((a, b) => a.score - b.score); + } + + return results.map(result => result.item); + } + + isMatch(item: string, query: string): number | undefined { + if (!this.options.caseSensitive) { + item = item.toLocaleLowerCase(); + query = query.toLocaleLowerCase(); + } + + const indexes = this.nearestIndexesFor(item, query); + + if (indexes === undefined) { + return; + } + // Exact matches should be first. + if (item === query) { + return 1; + } + // If we have more than 2 letters, matches close to each other should be first. + if (indexes.length > 1) { + return 2 + (indexes[indexes.length - 1] - indexes[0]); + } + // Matches closest to the start of the string should be first. + return 2 + indexes[0]; + } + + nearestIndexesFor(item: string, query: string): number[] | undefined { + const letters = query.split(''); + const indexes: number[][] = []; + + const idxFL = this.idxFirstLetter(item, query); + + for (const idx of idxFL) { + indexes.push([idx]); + for (let i = 1; i < letters.length; i++) { + const letter = letters[i]; + let index = item.indexOf(letter, idx + 1); + if (index === -1) { + indexes.pop(); + break; + } + indexes[indexes.length - 1].push(index); + index++; + } + } + + if (indexes.length === 0) { + return; + } + + return indexes.sort((a, b) => { + if (a.length === 1) { + return a[0] - b[0]; + } + + const res_a = a[a.length - 1] - a[0]; + const res_b = b[b.length - 1] - b[0]; + + return res_a - res_b; + })[0]; + } + + idxFirstLetter(item: string, query: string): number[] { + const match = query[0]; + + return item + .split('') + .map((letter, index) => { + if (letter !== match) { + return; + } + return index; + }) + .filter(index => index !== undefined) as number[]; + } +} + +class ReLibraryPlugin implements Plugin.PluginBase { + id = 'ReLib'; + name = 'Re:Library'; + icon = 'src/en/relibrary/icon.png'; + site = 'https://re-library.com'; + version = '1.0.3'; + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: 'https://re-library.com/', + }, + }; + + private searchFunc = new FuzzySearch<Plugin.NovelItem>(item => [item.name], { + sort: true, + caseSensitive: false, + }); + + private async popularNovelsInner(url: string): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + loadedCheerio('.entry-content > ol > li').each((_i, el) => { + const novel: Partial<Plugin.NovelItem> = {}; + novel.name = loadedCheerio(el).find('h3 > a').text(); + novel.path = loadedCheerio(el) + .find('table > tbody > tr > td > a') + .attr('href'); + if (novel.name === undefined || novel.path === undefined) return; + novel.cover = + loadedCheerio(el) + .find('table > tbody > tr > td > a > img') + .attr('data-cfsrc') || + loadedCheerio(el) + .find('table > tbody > tr > td > a > img') + .attr('src') || + defaultCover; + novel.path = new URL(novel.path, this.site).pathname; + novels.push(novel as Plugin.NovelItem); + }); + return novels; + } + + private async lastestNovelsInner(url: string): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + loadedCheerio('article.type-page.page').each((_i, el) => { + const novel: Partial<Plugin.NovelItem> = {}; + novel.name = loadedCheerio(el).find('.entry-title').text(); + novel.path = loadedCheerio(el).find('.entry-title a').attr('href'); + if (novel.path === undefined || novel.name === undefined) return; + novel.cover = + loadedCheerio(el) + .find('.entry-content > table > tbody > tr > td > a >img') + .attr('data-cfsrc') || + loadedCheerio(el) + .find('.entry-content > table > tbody > tr > td > a >img') + .attr('src') || + defaultCover; + novel.path = new URL(novel.path, this.site).pathname; + novels.push(novel as Plugin.NovelItem); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions<Filters>, + ): Promise<Plugin.NovelItem[]> { + // The most-popular page only has a single page, so we return an empty array in case you ask for the impossible + // the lastest page do have paginated result, so we support that. + if (showLatestNovels) + return this.lastestNovelsInner( + `${this.site}/tag/translations/page/${pageNo}`, + ); + else if (pageNo === 1) + return this.popularNovelsInner(`${this.site}/translations/most-popular/`); + else return []; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Partial<Plugin.SourceNovel> = { + path: novelPath, + }; + // title: .entry-content > header.entry-header > .entry-title + // img: .entry-content > table > tbody > tr > td > img + // tags: .entry-content > table > tbody > tr > td > p > span > a[] + // synopis: .entry-content > div.su-box > div.su-box-content + // chapters: .entry-content > div.su-accordion <then> li.page_item[] + + const result = await fetchApi(`${this.site}${novelPath}`); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + // If it doesn't find the name I should just throw an error (or early return) since the scraping is broken + novel.name = loadedCheerio('header.entry-header > .entry-title') + .text() + .trim(); + + if (novel.name === undefined || novel.name === '404 – Page not found') + throw new Error(`Invalid novel for url ${novelPath}`); + + // Find the cover + novel.cover = + loadedCheerio('.entry-content > table img').attr('data-cfsrc') || + loadedCheerio('.entry-content > table img').attr('src') || + defaultCover; + + novel.status = NovelStatus.Unknown; + loadedCheerio('.entry-content > table > tbody > tr > td > p').each( + function (_i, el) { + // Handle the novel status + // Sadly some novels just state the status inside the summary... + if ( + loadedCheerio(el) + .find('strong') + .text() + .toLowerCase() + .trim() + .startsWith('status') + ) { + loadedCheerio(el).find('strong').remove(); + const status = loadedCheerio(el).text().toLowerCase().trim(); + if (status.includes('on-going')) { + novel.status = NovelStatus.Ongoing; + } else if (status.includes('completed')) { + novel.status = NovelStatus.Completed; + } else if (status.includes('hiatus')) { + novel.status = NovelStatus.OnHiatus; + } else if (status.includes('cancelled')) { + novel.status = NovelStatus.Cancelled; + } else { + novel.status = loadCheerio(el).text(); + } + } + // Handle the genres + else if ( + loadedCheerio(el) + .find('strong') + .text() + .toLowerCase() + .trim() + .startsWith('Category') + ) { + loadedCheerio(el).find('strong').remove(); + novel.genres = loadedCheerio(el).text(); + } + }, + ); + + novel.summary = loadedCheerio( + '.entry-content > div.su-box > div.su-box-content', + ).text(); + + const chapters: Plugin.ChapterItem[] = []; + + let chapter_idx = 0; + loadedCheerio('.entry-content > div.su-accordion').each((_i1, el) => { + loadedCheerio(el) + .find('li > a') + .each((_i2, chap_el) => { + chapter_idx += 1; + const chap_path = loadedCheerio(chap_el).attr('href')?.trim(); + if (loadedCheerio(chap_el).text() === undefined || !chap_path) return; + chapters.push({ + name: loadedCheerio(chap_el).text(), + path: new URL(chap_path, this.site).pathname, + chapterNumber: chapter_idx, + // we KNOW that we can't get the released time (at least without any additional fetches), so set it to null purposfully + releaseTime: null, + }); + }); + }); + + novel.chapters = chapters; + return novel as Plugin.NovelItem; + } + + async parseChapter(chapterPath: string): Promise<string> { + // parse chapter text here + const result = await fetchApi(`${this.site}${chapterPath}`); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + const entryContent = loadedCheerio('.entry-content'); + const pageLinkHr = entryContent.find('.PageLink + hr').first(); + if (pageLinkHr.length) { + // Remove all previous siblings before the .PageLink + hr + let prev = pageLinkHr.prev(); + while (prev.length) { + prev.remove(); + prev = pageLinkHr.prev(); + } + const pageLink = pageLinkHr.prev('.PageLink'); + if (pageLink.length) { + pageLink.remove(); + } + pageLinkHr.next().remove(); + pageLinkHr.remove(); + } + + // Find the first <hr> followed by a .PageLink and remove everything after + const hrAfterPageLink = entryContent.find('hr + .PageLink').first(); + if (hrAfterPageLink.length) { + let next = hrAfterPageLink.next(); + while (next.length) { + const temp = next.next(); + next.remove(); + next = temp; + } + hrAfterPageLink.prev().remove(); + hrAfterPageLink.remove(); + } + + return entryContent.html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + // We only want to serve a single "page" since we do the search client side. + if (pageNo !== 1) return []; + + const novels: Plugin.NovelItem[] = []; + const req = await fetchApi(`${this.site}/translations/`); + const body = await req.text(); + + const loadedCheerio = loadCheerio(body); + + loadedCheerio('article article').each((_i, el) => { + const e = loadedCheerio(el); + const href = e.find('a').attr('href'); + if (href && e.find('a').text()) { + novels.push({ + name: e.find('h4').text(), + path: new URL(href, this.site).pathname, + cover: + e.find('img').attr('data-cfsrc') || + e.find('img').attr('src') || + defaultCover, + }); + } + }); + this.searchFunc.setHaystack(novels); + return this.searchFunc.search(searchTerm); + } +} + +export default new ReLibraryPlugin(); diff --git a/plugins/english/royalroad.ts b/plugins/english/royalroad.ts new file mode 100644 index 000000000..6fcaeda6c --- /dev/null +++ b/plugins/english/royalroad.ts @@ -0,0 +1,965 @@ +import { Parser } from 'htmlparser2'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { isUrlAbsolute } from '@libs/isAbsoluteUrl'; +import { storage } from '@libs/storage'; + +class RoyalRoad implements Plugin.PluginBase { + id = 'royalroad'; + name = 'Royal Road'; + version = '2.3.1'; + icon = 'src/en/royalroad/icon.png'; + site = 'https://www.royalroad.com/'; + + enableVol = storage.get('enableVol'); + pluginSettings = { + enableVol: { + value: '', + label: 'Enable Pagination / Volume view (not recommended)', + type: 'Switch', + }, + }; + + parseNovels(html: string) { + const baseUrl = this.site; + const novels: Plugin.NovelItem[] = []; + let tempNovel: Partial<Plugin.NovelItem> = {}; + let state: ParsingState = ParsingState.Idle; + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class']?.includes('fiction-list-item')) { + state = ParsingState.Novel; + } + if (state !== ParsingState.Novel) return; + + switch (name) { + case 'a': + if (attribs['href']) { + tempNovel.path = attribs['href'].split('/').slice(1, 3).join('/'); + } + break; + case 'img': + if (attribs['src']) { + tempNovel.name = attribs['alt'] || ''; + tempNovel.cover = !isUrlAbsolute(attribs['src']) + ? baseUrl + attribs['src'].slice(1) + : attribs['src']; + } + break; + } + }, + onclosetag(name) { + if (name === 'figure') { + if (tempNovel.path && tempNovel.name) { + novels.push(tempNovel as Plugin.NovelItem); + tempNovel = {}; + } + state = ParsingState.Idle; + } + }, + }); + + parser.write(html); + parser.end(); + + return novels; + } + + async popularNovels( + page: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + page: page.toString(), + }); + if (showLatestNovels) { + params.append('orderBy', 'last_update'); + } + if (!filters) filters = this.filters || {}; + for (const key in filters) { + if (filters[key as keyof typeof filters].value === '') continue; + if (key === 'genres' || key === 'tags' || key === 'content_warnings') { + if (filters[key].value.include) { + for (const include of filters[key].value.include) { + params.append('tagsAdd', include); + } + } + if (filters[key].value.exclude) { + for (const exclude of filters[key].value.exclude) { + params.append('tagsRemove', exclude); + } + } + } else { + params.append(key, String(filters[key as keyof typeof filters].value)); + } + } + + const link = `${this.site}fictions/search?${params.toString()}`; + const body = await fetchApi(link).then(r => r.text()); + + return this.parseNovels(body); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const html = await result.text(); + const novel: Partial<Plugin.SourceNovel> = { + path: novelPath, + }; + const baseUrl = this.site; + const enableVolume = this.enableVol; + + let state: ParsingState = ParsingState.Idle; + let statusText = ''; + let statusSpanCounter = 0; + + const nameParts: string[] = []; + const summaryParts: string[] = []; + const scriptContentParts: string[] = []; + const genreArray: string[] = []; + + let chapterJson: ChapterEntry[] = []; + let volumeJson: VolumeEntry[] = []; + + const parser = new Parser({ + onopentag(name, attribs) { + switch (name) { + case 'h1': + state = ParsingState.InTitle; + break; + case 'a': + if (attribs['href']?.startsWith('/profile/') && !novel.author) { + state = ParsingState.InAuthor; + } else if (state === ParsingState.InTags) { + state = ParsingState.InTagLink; + } + break; + case 'div': + if (attribs['class'] === 'description') { + state = ParsingState.InDescription; + } + break; + case 'hr': + if (state === ParsingState.InDescription) { + summaryParts.push('\n\n---\n\n'); + } + break; + case 'br': + if (state === ParsingState.InDescription) { + summaryParts.push('\n\n'); + } + break; + case 'span': + if (attribs['class']?.includes('tags')) { + state = ParsingState.InTags; + } else if (attribs['class']?.includes('label-sm')) { + statusSpanCounter++; + if (statusSpanCounter === 2) { + state = ParsingState.InStatusSpan; + statusText = ''; + } + } + break; + case 'img': + if (attribs['class']?.includes('thumbnail')) { + novel.cover = attribs['src']; + if (novel.cover && !isUrlAbsolute(novel.cover)) { + novel.cover = baseUrl + novel.cover.slice(1); + } + } + break; + case 'script': + state = ParsingState.InScript; + break; + } + }, + ontext(text) { + const trimmedText = text.trim(); + if (!trimmedText && state !== ParsingState.InScript) return; + + switch (state) { + case ParsingState.InTitle: + nameParts.push(text); + break; + case ParsingState.InAuthor: + novel.author = trimmedText; + break; + case ParsingState.InDescription: + summaryParts.push(text); + break; + case ParsingState.InStatusSpan: + statusText = trimmedText; + break; + case ParsingState.InTagLink: + genreArray.push(trimmedText); + break; + case ParsingState.InScript: + scriptContentParts.push(text); + break; + } + }, + onclosetag(name) { + switch (name) { + case 'h1': + if (state === ParsingState.InTitle) { + novel.name = nameParts.join('').trim(); + state = ParsingState.Idle; + } + break; + case 'a': + if (state === ParsingState.InTagLink) { + state = ParsingState.InTags; + } else if (state === ParsingState.InAuthor) { + state = ParsingState.Idle; + } + break; + case 'p': + if (state === ParsingState.InDescription) { + summaryParts.push('\n\n'); + } + break; + case 'div': + if (state === ParsingState.InDescription) { + novel.summary = summaryParts + .join('') + .replace(/ /g, ' ') + .replace(/\n{3,}/g, '\n\n') + .trim(); + summaryParts.length = 0; + state = ParsingState.Idle; + } + break; + case 'span': + if (state === ParsingState.InTags) { + novel.genres = genreArray.join(', '); + state = ParsingState.Idle; + } else if (state === ParsingState.InStatusSpan) { + state = ParsingState.Idle; + } + break; + case 'script': + if (state === ParsingState.InScript) { + state = ParsingState.Idle; + const scriptContent = scriptContentParts.join(''); + const chapterMatch = scriptContent.match( + /window\.chapters\s*=\s*(\[.*?\]);/, + ); + const volumeMatch = scriptContent.match( + /window\.volumes\s*=\s*(\[.*?\]);/, + ); + + if (chapterMatch?.[1]) { + chapterJson = JSON.parse(chapterMatch[1]); + } + if (volumeMatch?.[1] && enableVolume) { + volumeJson = JSON.parse(volumeMatch[1]); + } + } + break; + } + }, + onend() { + switch (statusText) { + case 'ONGOING': + novel.status = NovelStatus.Ongoing; + break; + case 'HIATUS': + novel.status = NovelStatus.OnHiatus; + break; + case 'COMPLETED': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + } + + novel.chapters = chapterJson.map((chapter: ChapterEntry) => { + const matchingVolume = volumeJson.find( + (volume: VolumeEntry) => volume.id === chapter.volumeId, + ); + return { + name: chapter.title, + path: (() => { + const parts = chapter.url.split('/'); + return `${parts[1]}/${parts[2]}/${parts[4]}/${parts[5]}`; + })(), + releaseTime: chapter.date, + chapterNumber: chapter?.order, + page: matchingVolume?.title, + }; + }); + }, + }); + + parser.write(html); + parser.end(); + + return novel as Plugin.NovelItem; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const html = await result.text(); + + let state = ParsingState.Idle; + let stateDepth = 0; + let depth = 0; + + const chapterHtmlParts: string[] = []; + const notesHtmlParts: string[] = []; + const beforeNotesParts: string[] = []; + const afterNotesParts: string[] = []; + let isBeforeChapter = true; + + const match = html.match(/<style>\n\s+.(.+?){[^{]+?display: none;/); + const hiddenClass = match?.[1]?.trim(); + let stateBeforeHidden: { + state: ParsingState; + depth: number; + } | null = null; + + type EscapeChar = '&' | '<' | '>' | '"' | "'"; + const escapeRegex = /[&<>"']/g; + const escapeMap: Record<EscapeChar, string> = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + }; + const escapeHtml = (text: string): string => + escapeRegex.test(text) + ? ((escapeRegex.lastIndex = 0), + text.replace(escapeRegex, char => escapeMap[char as EscapeChar])) + : text; + + const parser = new Parser({ + onopentag(name, attribs) { + depth++; + const classes = attribs['class'] || ''; + + if ( + state !== ParsingState.InHidden && + hiddenClass && + classes.includes(hiddenClass) + ) { + stateBeforeHidden = { state: state, depth: stateDepth }; + state = ParsingState.InHidden; + stateDepth = depth; + return; + } + + switch (state) { + case ParsingState.Idle: + if (classes.includes('chapter-content')) { + state = ParsingState.InChapter; + stateDepth = depth; + isBeforeChapter = false; + } else if (classes.includes('author-note-portlet')) { + state = ParsingState.InNote; + stateDepth = depth; + } + break; + case ParsingState.InHidden: + return; + } + + if (state === ParsingState.InChapter || state === ParsingState.InNote) { + let tag = `<${name}`; + for (const attr in attribs) { + const value = attribs[attr].replace(/"/g, '"'); + tag += ` ${attr}="${value}"`; + } + tag += '>'; + + if (state === ParsingState.InChapter) { + chapterHtmlParts.push(tag); + } else { + notesHtmlParts.push(tag); + } + } + }, + ontext(text) { + switch (state) { + case ParsingState.InChapter: + chapterHtmlParts.push(escapeHtml(text)); + break; + case ParsingState.InNote: + notesHtmlParts.push(escapeHtml(text)); + break; + } + }, + onclosetag(name) { + if (depth === stateDepth) { + switch (state) { + case ParsingState.InHidden: + if (!stateBeforeHidden) { + state = ParsingState.Idle; // Attempt recovery + stateDepth = 0; + } else { + state = stateBeforeHidden.state; + stateDepth = stateBeforeHidden.depth; + stateBeforeHidden = null; + } + depth--; + return; + case ParsingState.InChapter: + chapterHtmlParts.push(`</div>`); + state = ParsingState.Idle; + stateDepth = 0; + depth--; + return; + case ParsingState.InNote: + { + const noteClass = `author-note-${isBeforeChapter ? 'before' : 'after'}`; + const notesHtml = notesHtmlParts.join('').trim(); + const fullNote = `<div class="${noteClass}">${notesHtml}</div>`; + if (isBeforeChapter) { + beforeNotesParts.push(fullNote); + } else { + afterNotesParts.push(fullNote); + } + notesHtmlParts.length = 0; + state = ParsingState.Idle; + stateDepth = 0; + depth--; + } + return; + } + } else if ( + state === ParsingState.InChapter || + state === ParsingState.InNote + ) { + if (!parser['isVoidElement'](name)) { + const closingTag = `</${name}>`; + if (state === ParsingState.InChapter) { + chapterHtmlParts.push(closingTag); + } else { + notesHtmlParts.push(closingTag); + } + } + } + depth--; + }, + }); + + parser.write(html); + parser.end(); + + return [ + beforeNotesParts.length > 0 ? beforeNotesParts.join('') : null, + chapterHtmlParts.length > 0 ? chapterHtmlParts.join('').trim() : null, + afterNotesParts.length > 0 ? afterNotesParts.join('') : null, + ] + .filter(Boolean) + .join('\n<hr class="notes-separator">\n'); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + page: page.toString(), + title: searchTerm, + globalFilters: 'true', + }); + const searchUrl = `${this.site}fictions/search?${params.toString()}`; + const body = await fetchApi(searchUrl).then(r => r.text()); + + return this.parseNovels(body); + } + + filters = { + 'keyword': { + 'type': FilterTypes.TextInput, + 'label': 'Keyword (title or description)', + 'value': '', + }, + 'author': { + 'type': FilterTypes.TextInput, + 'label': 'Author', + 'value': '', + }, + 'genres': { + 'type': FilterTypes.ExcludableCheckboxGroup, + 'label': 'Genres', + 'value': { + 'include': [], + 'exclude': [], + }, + 'options': [ + { + 'label': 'Action', + 'value': 'action', + }, + { + 'label': 'Adventure', + 'value': 'adventure', + }, + { + 'label': 'Comedy', + 'value': 'comedy', + }, + { + 'label': 'Contemporary', + 'value': 'contemporary', + }, + { + 'label': 'Drama', + 'value': 'drama', + }, + { + 'label': 'Fantasy', + 'value': 'fantasy', + }, + { + 'label': 'Historical', + 'value': 'historical', + }, + { + 'label': 'Horror', + 'value': 'horror', + }, + { + 'label': 'Mystery', + 'value': 'mystery', + }, + { + 'label': 'Psychological', + 'value': 'psychological', + }, + { + 'label': 'Romance', + 'value': 'romance', + }, + { + 'label': 'Satire', + 'value': 'satire', + }, + { + 'label': 'Sci-fi', + 'value': 'sci_fi', + }, + { + 'label': 'Short Story', + 'value': 'one_shot', + }, + { + 'label': 'Tragedy', + 'value': 'tragedy', + }, + ], + }, + 'tags': { + 'type': FilterTypes.ExcludableCheckboxGroup, + 'label': 'Tags', + 'value': { + 'include': [], + 'exclude': [], + }, + 'options': [ + { + 'label': 'Anti-Hero Lead', + 'value': 'anti-hero_lead', + }, + { + 'label': 'Artificial Intelligence', + 'value': 'artificial_intelligence', + }, + { + 'label': 'Attractive Lead', + 'value': 'attractive_lead', + }, + { + 'label': 'Cyberpunk', + 'value': 'cyberpunk', + }, + { + 'label': 'Dungeon', + 'value': 'dungeon', + }, + { + 'label': 'Dystopia', + 'value': 'dystopia', + }, + { + 'label': 'Female Lead', + 'value': 'female_lead', + }, + { + 'label': 'First Contact', + 'value': 'first_contact', + }, + { + 'label': 'GameLit', + 'value': 'gamelit', + }, + { + 'label': 'Gender Bender', + 'value': 'gender_bender', + }, + { + 'label': 'Genetically Engineered', + 'value': 'genetically_engineered ', + }, + { + 'label': 'Grimdark', + 'value': 'grimdark', + }, + { + 'label': 'Hard Sci-fi', + 'value': 'hard_sci-fi', + }, + { + 'label': 'Harem', + 'value': 'harem', + }, + { + 'label': 'High Fantasy', + 'value': 'high_fantasy', + }, + { + 'label': 'LitRPG', + 'value': 'litrpg', + }, + { + 'label': 'Low Fantasy', + 'value': 'low_fantasy', + }, + { + 'label': 'Magic', + 'value': 'magic', + }, + { + 'label': 'Male Lead', + 'value': 'male_lead', + }, + { + 'label': 'Martial Arts', + 'value': 'martial_arts', + }, + { + 'label': 'Multiple Lead Characters', + 'value': 'multiple_lead', + }, + { + 'label': 'Mythos', + 'value': 'mythos', + }, + { + 'label': 'Non-Human Lead', + 'value': 'non-human_lead', + }, + { + 'label': 'Portal Fantasy / Isekai', + 'value': 'summoned_hero', + }, + { + 'label': 'Post Apocalyptic', + 'value': 'post_apocalyptic', + }, + { + 'label': 'Progression', + 'value': 'progression', + }, + { + 'label': 'Reader Interactive', + 'value': 'reader_interactive', + }, + { + 'label': 'Reincarnation', + 'value': 'reincarnation', + }, + { + 'label': 'Ruling Class', + 'value': 'ruling_class', + }, + { + 'label': 'School Life', + 'value': 'school_life', + }, + { + 'label': 'Secret Identity', + 'value': 'secret_identity', + }, + { + 'label': 'Slice of Life', + 'value': 'slice_of_life', + }, + { + 'label': 'Soft Sci-fi', + 'value': 'soft_sci-fi', + }, + { + 'label': 'Space Opera', + 'value': 'space_opera', + }, + { + 'label': 'Sports', + 'value': 'sports', + }, + { + 'label': 'Steampunk', + 'value': 'steampunk', + }, + { + 'label': 'Strategy', + 'value': 'strategy', + }, + { + 'label': 'Strong Lead', + 'value': 'strong_lead', + }, + { + 'label': 'Super Heroes', + 'value': 'super_heroes', + }, + { + 'label': 'Supernatural', + 'value': 'supernatural', + }, + { + 'label': 'Technologically Engineered', + 'value': 'technologically_engineered', + }, + { + 'label': 'Time Loop', + 'value': 'loop', + }, + { + 'label': 'Time Travel', + 'value': 'time_travel', + }, + { + 'label': 'Urban Fantasy', + 'value': 'urban_fantasy', + }, + { + 'label': 'Villainous Lead', + 'value': 'villainous_lead', + }, + { + 'label': 'Virtual Reality', + 'value': 'virtual_reality', + }, + { + 'label': 'War and Military', + 'value': 'war_and_military', + }, + { + 'label': 'Wuxia', + 'value': 'wuxia', + }, + { + 'label': 'Xianxia', + 'value': 'xianxia', + }, + ], + }, + 'content_warnings': { + 'type': FilterTypes.ExcludableCheckboxGroup, + 'label': 'Content Warnings', + 'value': { + 'include': [], + 'exclude': [], + }, + 'options': [ + { + 'label': 'Profanity', + 'value': 'profanity', + }, + { + 'label': 'Sexual Content', + 'value': 'sexuality', + }, + { + 'label': 'Graphic Violence', + 'value': 'graphic_violence', + }, + { + 'label': 'Sensitive Content', + 'value': 'sensitive', + }, + { + 'label': 'AI-Assisted Content', + 'value': 'ai_assisted', + }, + { + 'label': 'AI-Generated Content', + 'value': 'ai_generated', + }, + ], + }, + 'minPages': { + 'type': FilterTypes.TextInput, + 'label': 'Min Pages', + 'value': '0', + }, + 'maxPages': { + 'type': FilterTypes.TextInput, + 'label': 'Max Pages', + 'value': '20000', + }, + 'minRating': { + 'type': FilterTypes.TextInput, + 'label': 'Min Rating (0.0 - 5.0)', + 'value': '0.0', + }, + 'maxRating': { + 'type': FilterTypes.TextInput, + 'label': 'Max Rating (0.0 - 5.0)', + 'value': '5.0', + }, + 'status': { + 'type': FilterTypes.Picker, + 'label': 'Status', + 'value': 'ALL', + 'options': [ + { + 'label': 'All', + 'value': 'ALL', + }, + { + 'label': 'Completed', + 'value': 'COMPLETED', + }, + { + 'label': 'Dropped', + 'value': 'DROPPED', + }, + { + 'label': 'Ongoing', + 'value': 'ONGOING', + }, + { + 'label': 'Hiatus', + 'value': 'HIATUS', + }, + { + 'label': 'Stub', + 'value': 'STUB', + }, + ], + }, + 'orderBy': { + 'type': FilterTypes.Picker, + 'label': 'Order by', + 'value': 'relevance', + 'options': [ + { + 'label': 'Relevance', + 'value': 'relevance', + }, + { + 'label': 'Popularity', + 'value': 'popularity', + }, + { + 'label': 'Average Rating', + 'value': 'rating', + }, + { + 'label': 'Last Update', + 'value': 'last_update', + }, + { + 'label': 'Release Date', + 'value': 'release_date', + }, + { + 'label': 'Followers', + 'value': 'followers', + }, + { + 'label': 'Number of Pages', + 'value': 'length', + }, + { + 'label': 'Views', + 'value': 'views', + }, + { + 'label': 'Title', + 'value': 'title', + }, + { + 'label': 'Author', + 'value': 'author', + }, + ], + }, + 'dir': { + 'type': FilterTypes.Picker, + 'label': 'Direction', + 'value': 'desc', + 'options': [ + { + 'label': 'Ascending', + 'value': 'asc', + }, + { + 'label': 'Descending', + 'value': 'desc', + }, + ], + }, + 'type': { + 'type': FilterTypes.Picker, + 'label': 'Type', + 'value': 'ALL', + 'options': [ + { + 'label': 'All', + 'value': 'ALL', + }, + { + 'label': 'Fan Fiction', + 'value': 'fanfiction', + }, + { + 'label': 'Original', + 'value': 'original', + }, + ], + }, + } satisfies Filters; +} + +export default new RoyalRoad(); + +type ChapterEntry = { + id: number; + volumeId: number; + title: string; + date: string; + order: number; + url: string; +}; + +type VolumeEntry = { + id: number; + title: string; + cover: string; + order: number; +}; + +enum ParsingState { + Idle, + InTitle, + InAuthor, + InDescription, + InTags, + InTagLink, + InStatusSpan, + InScript, + InNote, + InChapter, + InHidden, + Novel, +} diff --git a/plugins/english/scribblehub.ts b/plugins/english/scribblehub.ts new file mode 100644 index 000000000..6f9d6b558 --- /dev/null +++ b/plugins/english/scribblehub.ts @@ -0,0 +1,303 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import dayjs from 'dayjs'; + +class ScribbleHubPlugin implements Plugin.PluginBase { + id = 'scribblehub'; + name = 'Scribble Hub'; + icon = 'src/en/scribblehub/icon.png'; + site = 'https://www.scribblehub.com/'; + version = '1.0.2'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.search_main_box').each((i, el) => { + const novelName = loadedCheerio(el).find('.search_title > a').text(); + const novelCover = loadedCheerio(el) + .find('.search_img > img') + .attr('src'); + const novelUrl = loadedCheerio(el).find('.search_title > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + }); + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = `${this.site}`; + if (showLatestNovels) { + url += `latest-series/?pg=${page}`; + } else if (filters) { + const params = new URLSearchParams(); + if (filters.genres.value.include?.length) { + params.append('gi', filters.genres.value.include.join(',')); + } + if ( + filters.genres.value.include?.length || + filters.genres.value.exclude?.length + ) { + params.append('mgi', filters.genre_operator.value); + } + if (filters.genres.value.exclude?.length) { + params.append('ge', filters.genres.value.exclude.join(',')); + } + if (filters.content_warning.value.include?.length) { + params.append('cti', filters.content_warning.value.include.join(',')); + } + if ( + filters.content_warning.value.include?.length || + filters.content_warning.value.exclude?.length + ) { + params.append('mct', filters.content_warning_operator.value); + } + if (filters.content_warning.value.exclude?.length) { + params.append('cte', filters.content_warning.value.exclude.join(',')); + } + params.append('cp', filters.storyStatus.value); + params.append('sort', filters.sort.value); + params.append('order', filters.order.value); + params.append('pg', page.toString()); + url += `series-finder/?sf=1&${params.toString()}`; + } else { + url += `series-finder/?sf=1&sort=ratings&order=desc&pg=${page}`; + } + + const body = await fetchApi(url).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.fic_title').text() || 'Untitled', + cover: loadedCheerio('.fic_image > img').attr('src'), + summary: loadedCheerio('.wi_fic_desc').text(), + author: loadedCheerio('.auth_name_fic').text(), + chapters: [], + }; + + novel.genres = loadedCheerio('.fic_genre') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('.rnd_stats').next().text().includes('Ongoing') + ? 'Ongoing' + : 'Completed'; + + const formData = new FormData(); + formData.append('action', 'wi_getreleases_pagination'); + formData.append('pagenum', '-1'); + formData.append('mypostid', novelPath.split('/')[1]); + + const data = await fetchApi(`${this.site}wp-admin/admin-ajax.php`, { + method: 'POST', + body: formData, + }); + const text = await data.text(); + + loadedCheerio = parseHTML(text); + + const chapter: Plugin.ChapterItem[] = []; + + const parseISODate = (date: string) => { + if (date.includes('ago')) { + const dayJSDate = dayjs(new Date()); // today + const timeAgo = date.match(/\d+/)?.[0] || ''; + const timeAgoInt = parseInt(timeAgo, 10); + + if (!timeAgo) return null; // there is no number! + + if (date.includes('hours ago') || date.includes('hour ago')) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } + + if (date.includes('days ago') || date.includes('day ago')) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } + + if (date.includes('months ago') || date.includes('month ago')) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } + + return dayJSDate.toISOString(); + } + return null; + }; + + loadedCheerio('.toc_w').each((i, el) => { + const chapterName = loadedCheerio(el).find('.toc_a').text(); + const releaseDate = loadedCheerio(el).find('.fic_date_pub').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + chapter.push({ + name: chapterName, + releaseTime: parseISODate(releaseDate), + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.chp_raw').html() || ''; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}?s=${encodeURIComponent(searchTerm)}&post_type=fictionposts`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + sort: { + label: 'Sort Results By', + value: 'ratings', + options: [ + { label: 'Chapters', value: 'chapters' }, + { label: 'Chapters per Week', value: 'frequency' }, + { label: 'Date Added', value: 'dateadded' }, + { label: 'Favorites', value: 'favorites' }, + { label: 'Last Updated', value: 'lastchdate' }, + { label: 'Number of Ratings', value: 'numofrate' }, + { label: 'Pages', value: 'pages' }, + { label: 'Pageviews', value: 'pageviews' }, + { label: 'Ratings', value: 'ratings' }, + { label: 'Readers', value: 'readers' }, + { label: 'Reviews', value: 'reviews' }, + { label: 'Total Words', value: 'totalwords' }, + ], + type: FilterTypes.Picker, + }, + order: { + label: 'Order By', + value: 'desc', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + storyStatus: { + label: 'Story Status', + value: 'all', + options: [ + { label: 'All', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Hiatus', value: 'hiatus' }, + ], + type: FilterTypes.Picker, + }, + genre_operator: { + value: 'and', + label: 'Genres (And/Or)', + options: [ + { label: 'And', value: 'and' }, + { label: 'Or', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Action', value: '9' }, + { label: 'Adult', value: '902' }, + { label: 'Adventure', value: '8' }, + { label: 'Boys Love', value: '891' }, + { label: 'Comedy', value: '7' }, + { label: 'Drama', value: '903' }, + { label: 'Ecchi', value: '904' }, + { label: 'Fanfiction', value: '38' }, + { label: 'Fantasy', value: '19' }, + { label: 'Gender Bender', value: '905' }, + { label: 'Girls Love', value: '892' }, + { label: 'Harem', value: '1015' }, + { label: 'Historical', value: '21' }, + { label: 'Horror', value: '22' }, + { label: 'Isekai', value: '37' }, + { label: 'Josei', value: '906' }, + { label: 'LitRPG', value: '1180' }, + { label: 'Martial Arts', value: '907' }, + { label: 'Mature', value: '20' }, + { label: 'Mecha', value: '908' }, + { label: 'Mystery', value: '909' }, + { label: 'Psychological', value: '910' }, + { label: 'Romance', value: '6' }, + { label: 'School Life', value: '911' }, + { label: 'Sci-fi', value: '912' }, + { label: 'Seinen', value: '913' }, + { label: 'Slice of Life', value: '914' }, + { label: 'Smut', value: '915' }, + { label: 'Sports', value: '916' }, + { label: 'Supernatural', value: '5' }, + { label: 'Tragedy', value: '901' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + content_warning_operator: { + value: 'and', + label: 'Mature Content (And/Or)', + options: [ + { label: 'And', value: 'and' }, + { label: 'Or', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + content_warning: { + value: { + include: [], + exclude: [], + }, + label: 'Mature Content', + options: [ + { label: 'Gore', value: '48' }, + { label: 'Sexual Content', value: '50' }, + { label: 'Strong Language', value: '49' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + } satisfies Filters; +} + +export default new ScribbleHubPlugin(); diff --git a/plugins/english/vynovel.ts b/plugins/english/vynovel.ts new file mode 100644 index 000000000..9f0d2564f --- /dev/null +++ b/plugins/english/vynovel.ts @@ -0,0 +1,172 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs, { ManipulateType } from 'dayjs'; + +class VyNovel implements Plugin.PluginBase { + id = 'vynovel'; + name = 'VyNovel'; + site = 'https://vynovel.com'; + version = '1.0.1'; + icon = 'src/en/vynovel/icon.png'; + + async fetchNovels( + page: number, + showLatestNovels?: boolean, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + searchTerm?: string, + ): Promise<Plugin.NovelItem[]> { + const data = new URLSearchParams({ + sort: showLatestNovels ? 'updated_at' : filters?.sort?.value || 'viewed', + page: page.toString(), + }); + if (searchTerm) data.append('q', searchTerm); + + const url = this.site + '/search?' + data.toString(); + + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div[class="comic-item"] > a').each((_, element) => { + const name = loadedCheerio(element) + .find('div[class="comic-title"]') + .text() + ?.trim(); + const cover = + loadedCheerio(element) + .find('div[class="comic-image lozad "]') + .attr('data-background-image') || defaultCover; + const url = loadedCheerio(element).attr('href'); + + if (!name || !url) return; + + novels.push({ name, cover, path: url.replace('/novel/', '') }); + }); + + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + return this.fetchNovels(page, showLatestNovels, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, false, undefined, searchTerm); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1[class="title"]').text().trim(), + cover: + loadedCheerio('div[class="img-manga"] > img').attr('src') || + defaultCover, + summary: loadedCheerio('div[class="summary"] > p[class="content"]') + .text() + .trim(), + author: loadedCheerio('div[class="col-md-7"] > p:nth-child(5) > a') + .text() + .trim(), + status: + loadedCheerio('span[class="text-ongoing"]').text() === 'Ongoing' + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + const chapters: Plugin.ChapterItem[] = []; + const totalChapters = loadedCheerio('div[class="list-group"] > a').length; + + loadedCheerio('div[class="list-group"] > a').each( + (chapterIndex, element) => { + const name = loadedCheerio(element).find('span').text().trim(); + const id = loadedCheerio(element).attr('id')?.replace(/\D/g, ''); + if (!name || !id) return; + + const releaseDate = loadedCheerio(element).find('p').text(); + chapters.push({ + name, + path: novelPath + '/' + id, + releaseTime: this.parseAgoDate(releaseDate), + chapterNumber: totalChapters - chapterIndex, + }); + }, + ); + + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('.content').html(); + return chapterText || ''; + } + + private parseAgoDate(date: string | undefined) { + //parseMadaraDate + const parsed = dayjs(date); + if (date && parsed.isValid()) { + return parsed.toISOString(); + } + + const [amt, time, ago] = date?.toLowerCase().trim().split(/\s+/) || []; + const decade = time?.includes('decade'); // dayjs no support, but just in case + const amount = (amt === 'a' || amt === 'an' ? 1 : +amt) * (decade ? 10 : 1); + const unit = (decade ? 'year' : time) as ManipulateType; + + const validUnits = [ + 'millisecond', // waow + 'second', + 'minute', + 'hour', + 'day', + 'week', + 'month', + 'year', + ]; + + if (ago !== 'ago' || isNaN(amount) || !validUnits.includes(unit)) { + return null; + } + + return dayjs().subtract(amount, unit).toISOString(); + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/novel/' : '/read/') + path; + + filters = { + sort: { + label: 'Sort By:', + value: 'viewed', + options: [ + { label: 'Viewed', value: 'viewed' }, + { label: 'Scored', value: 'scored' }, + { label: 'Newest', value: 'created_at' }, + { label: 'Latest Update', value: 'updated_at' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new VyNovel(); diff --git a/plugins/english/wct.ts b/plugins/english/wct.ts new file mode 100644 index 000000000..b3906661c --- /dev/null +++ b/plugins/english/wct.ts @@ -0,0 +1,157 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; + +class WitchCultTranslations implements Plugin.PluginBase { + id = 'witchculttranslations'; + name = 'Witch Cult Translations'; + site = 'https://witchculttranslation.com'; + icon = 'src/en/wct/icon.png'; + version = '1.0.0'; + + private cachedNovel: Plugin.NovelItem | null = null; + + private async novel(): Promise<Plugin.NovelItem> { + if (this.cachedNovel !== null) { + return this.cachedNovel; + } + + const result = await fetchApi(this.site); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const latestArcCover = loadedCheerio('.entry-content h1 img') + .last() + .attr('src'); + + this.cachedNovel = { + name: 'Re:Zero kara Hajimeru Isekai Seikatsu', + path: '/table-of-content', + cover: latestArcCover, + }; + + return this.cachedNovel; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const novels = []; + if (pageNo === 1) { + novels.push(await this.novel()); + } + + return novels; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const novels = [await this.novel()]; + + const q = this.normalize(searchTerm); + + return novels.filter(({ name }) => this.normalize(name).includes(q)); + } + + private normalize(str: string) { + return str.toLowerCase().replace(/[^a-z0-9]/g, ''); + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + const title = loadedCheerio('h1.entry-title').text().trim(); + const content = loadedCheerio('.entry-content').first(); + content + .find('#patreon-snippet, .sharedaddy, .jp-relatedposts, #jp-post-flair') + .remove(); + + return `<h1>${title}</h1>${content.html() || ''}`; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const [body, novel] = await Promise.all([ + fetchApi(this.site + novelPath).then(result => result.text()), + this.novel(), + ]); + + const loadedCheerio = parseHTML(body); + + return { + ...novel, + author: 'Tappei Nagatsuki', + chapters: this.parseChaptersFromTOC(loadedCheerio), + status: NovelStatus.Ongoing, + summary: + 'Fan translation of the Re:Zero web novel (Arc 5 onwards).\n\nSuddenly, Natsuki Subaru, a shut-in student, is summoned to another world on his way home from the convenience store. A completely ordinary person with no knowledge, skills, combat abilities, or communication skills, he\'s thrown into this other world without any cheat bonuses and must desperately try to survive. The only blessing he receives is the painful ability to "return by death," which allows him to rewind time after dying! In this other world where he has no one to rely on, how many times will he die, and what will he ultimately gain?', + }; + } + + private parseChaptersFromTOC( + loadedCheerio: CheerioAPI, + ): Plugin.ChapterItem[] { + const chapters: Plugin.ChapterItem[] = []; + let currentArc = 0; + let chapterNumber = 0; + + const children = loadedCheerio('.entry-content') + .first() + .children() + .toArray(); + + for (const el of children) { + if (el.type !== 'tag') continue; + const tag = el.tagName.toLowerCase(); + + if (tag === 'h1' || tag === 'h2') { + const text = loadedCheerio(el).text().trim(); + const arcMatch = text.match(/^Arc\s+(\d+)/i); + if (arcMatch) { + currentArc = parseInt(arcMatch[1], 10); + continue; + } + if (/^Side Content/i.test(text)) { + break; + } + continue; + } + + if (tag !== 'ul' || currentArc < 5) continue; + + loadedCheerio(el) + .find('li > a') + .each((_, a) => { + const href = loadedCheerio(a).attr('href'); + if (!href) return; + + const onSite = + /^https?:\/\/(?:www\.)?witchculttranslation\.com\//i.test(href); + if (!onSite) return; + + const name = loadedCheerio(a).text().trim(); + if (!name) return; + + const path = `/${href + .replace(/^https?:\/\/(?:www\.)?witchculttranslation\.com\//i, '') + .replace(/^\/+/, '')}`; + + const dateMatch = path.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\//); + const releaseTime = dateMatch + ? `${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}` + : null; + + chapterNumber += 1; + chapters.push({ + name: `Arc ${currentArc}, ${name}`, + path, + releaseTime, + chapterNumber, + }); + }); + } + + return chapters; + } +} + +export default new WitchCultTranslations(); diff --git a/plugins/english/webnovel.ts b/plugins/english/webnovel.ts new file mode 100644 index 000000000..a1b9d36a8 --- /dev/null +++ b/plugins/english/webnovel.ts @@ -0,0 +1,340 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { storage } from '@libs/storage'; + +class Webnovel implements Plugin.PluginBase { + id = 'webnovel'; + name = 'Webnovel'; + version = '1.0.3'; + icon = 'src/en/webnovel/icon.png'; + site = 'https://www.webnovel.com'; + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }; + imageRequestInit?: Plugin.ImageRequestInit | undefined = { + headers: { + 'referrer': this.site, + }, + }; + hideLocked = storage.get('hideLocked'); + pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + + async parseNovels( + loadedCheerio: CheerioAPI, + category_bool: boolean, + search_bool: boolean, + ): Promise<Plugin.NovelItem[]> { + const selector = category_bool + ? '.j_category_wrapper' + : search_bool + ? '.j_list_container' + : ''; + const attribute = category_bool + ? 'data-original' + : search_bool + ? 'src' + : ''; + + return loadedCheerio(`${selector} li`) + .map((i_, ele) => { + const novelName = + loadedCheerio(ele).find('.g_thumb').attr('title') || 'No Title Found'; + const novelCover = loadedCheerio(ele) + .find('.g_thumb > img') + .attr(attribute); + const novelPath = loadedCheerio(ele).find('.g_thumb').attr('href'); + + if (!novelPath) return null; + + return { + name: novelName, + cover: 'https:' + novelCover, + path: novelPath, + }; + }) + .get() + .filter(novel => novel !== null); + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + if (filters?.fanfic_search.value) { + return this.searchNovelsInternal( + filters.fanfic_search.value, + pageNo, + 'fanfic', + ); + } + let url = this.site + '/stories/'; + const params = new URLSearchParams(); + + if (showLatestNovels) { + url += `novel?orderBy=5&pageIndex=${pageNo}`; + } else if (filters) { + if (filters.genres_gender.value === '1') { + if (filters.genres_male.value !== '1') { + url += filters.genres_male.value; + } else { + url += 'novel'; + params.append('gender', '1'); + } + } else if (filters.genres_gender.value === '2') { + if (filters.genres_female.value !== '2') { + url += filters.genres_female.value; + } else { + url += 'novel'; + params.append('gender', '2'); + } + } + + if (filters.type.value !== '3') { + params.append('sourceType', filters.type.value); + } else { + params.append('translateMode', '3'); + params.append('sourceType', '1'); + } + + params.append('bookStatus', filters.status.value); + params.append('orderBy', filters.sort.value); + params.append('pageIndex', pageNo.toString()); + + url += '?' + params.toString(); + } else { + url += `novel?orderBy=1&pageIndex=${pageNo}`; + } + + const result = await fetchApi(url, { + headers: this.headers, + }); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio, true, false); + } + + async parseChapters(novelPath: string): Promise<Plugin.ChapterItem[]> { + const url = this.site + novelPath + '/catalog'; + const result = await fetchApi(url, { + headers: this.headers, + }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.volume-item').each((i_v_, ele_v) => { + const originalVolumeName = loadedCheerio(ele_v).first().text().trim(); + const volumeNameMatch = originalVolumeName.match(/Volume\s(\d+)/); + const volumeName = volumeNameMatch + ? `Volume ${volumeNameMatch[1]}` + : 'Unknown Volume'; + + loadedCheerio(ele_v) + .find('li') + .each((i_c_, ele_c) => { + const chapterName = + `${volumeName}: ` + + (loadedCheerio(ele_c).find('a').attr('title')?.trim() || + 'No Title Found'); + const chapterPath = loadedCheerio(ele_c).find('a').attr('href'); + const locked = loadedCheerio(ele_c).find('svg').length; + + if (chapterPath && !(locked && this.hideLocked)) { + chapters.push({ + name: locked ? `${chapterName} 🔒` : chapterName, + path: chapterPath, + }); + } + }); + }); + + return chapters; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + const result = await fetchApi(url, { + headers: this.headers, + }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.g_thumb > img').attr('alt') || 'No Title Found', + cover: 'https:' + loadedCheerio('.g_thumb > img').attr('src'), + genres: loadedCheerio('.det-hd-detail > .det-hd-tag').attr('title') || '', + summary: + loadedCheerio('.j_synopsis > p') + .find('br') + .replaceWith('\n') + .end() + .text() + .trim() || 'No Summary Found', + author: + loadedCheerio('.det-info .c_s') + .filter((i_, ele) => { + return loadedCheerio(ele).text().trim() === 'Author:'; + }) + .next() + .text() + .trim() || 'No Author Found', + status: + loadedCheerio('.det-hd-detail svg') + .filter((i_, ele) => { + return loadedCheerio(ele).attr('title') === 'Status'; + }) + .next() + .text() + .trim() || 'Unknown Status', + chapters: await this.parseChapters(novelPath), + }; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + const result = await fetchApi(url, { + headers: this.headers, + }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const bloatElements = ['.para-comment']; + bloatElements.forEach(tag => loadedCheerio(tag).remove()); + + return ( + loadedCheerio('.cha-tit').html()! + loadedCheerio('.cha-words').html()! + ); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + return this.searchNovelsInternal(searchTerm, pageNo); + } + + async searchNovelsInternal( + searchTerm: string, + pageNo: number, + type?: string, + ): Promise<Plugin.NovelItem[]> { + searchTerm = searchTerm.replace(/\s+/g, '+'); + + const url = `${this.site}/search?keywords=${encodeURIComponent(searchTerm)}&pageIndex=${pageNo}${type ? `&type=${type}` : ''}`; + const result = await fetchApi(url, { + headers: this.headers, + }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio, false, true); + } + + filters = { + sort: { + label: 'Sort Results By', + value: '1', + options: [ + { label: 'Popular', value: '1' }, + { label: 'Recommended', value: '2' }, + { label: 'Most Collections', value: '3' }, + { label: 'Rating', value: '4' }, + { label: 'Time Updated', value: '5' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Content Status', + value: '0', + options: [ + { label: 'All', value: '0' }, + { label: 'Completed', value: '2' }, + { label: 'Ongoing', value: '1' }, + ], + type: FilterTypes.Picker, + }, + genres_gender: { + label: 'Genres (Male/Female)', + value: '1', + options: [ + { label: 'Male', value: '1' }, + { label: 'Female', value: '2' }, + ], + type: FilterTypes.Picker, + }, + genres_male: { + label: 'Male Genres', + value: '1', + options: [ + { label: 'All', value: '1' }, + { label: 'Action', value: 'novel-action-male' }, + { label: 'Animation, Comics, Games', value: 'novel-acg-male' }, + { label: 'Eastern', value: 'novel-eastern-male' }, + { label: 'Fantasy', value: 'novel-fantasy-male' }, + { label: 'Games', value: 'novel-games-male' }, + { label: 'History', value: 'novel-history-male' }, + { label: 'Horror', value: 'novel-horror-male' }, + { label: 'Realistic', value: 'novel-realistic-male' }, + { label: 'Sci-fi', value: 'novel-scifi-male' }, + { label: 'Sports', value: 'novel-sports-male' }, + { label: 'Urban', value: 'novel-urban-male' }, + { label: 'War', value: 'novel-war-male' }, + ], + type: FilterTypes.Picker, + }, + genres_female: { + label: 'Female Genres', + value: '2', + options: [ + { label: 'All', value: '2' }, + { label: 'Fantasy', value: 'novel-fantasy-female' }, + { label: 'General', value: 'novel-general-female' }, + { label: 'History', value: 'novel-history-female' }, + { label: 'LGBT+', value: 'novel-lgbt-female' }, + { label: 'Sci-fi', value: 'novel-scifi-female' }, + { label: 'Teen', value: 'novel-teen-female' }, + { label: 'Urban', value: 'novel-urban-female' }, + ], + type: FilterTypes.Picker, + }, + type: { + label: 'Content Type', + value: '0', + options: [ + { label: 'All', value: '0' }, + { label: 'Translate', value: '1' }, + { label: 'Original', value: '2' }, + { label: 'MTL (Machine Translation)', value: '3' }, + ], + type: FilterTypes.Picker, + }, + fanfic_search: { + label: 'Search fanfics (Overrides other filters)', + value: '', + type: FilterTypes.TextInput, + }, + } satisfies Filters; +} + +export default new Webnovel(); diff --git a/plugins/english/wntl.ts b/plugins/english/wntl.ts new file mode 100644 index 000000000..8ff6b21cd --- /dev/null +++ b/plugins/english/wntl.ts @@ -0,0 +1,163 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class WNTLPlugin implements Plugin.PluginBase { + id = 'wntl'; + name = 'WNTL'; + icon = 'src/en/wntl/icon.png'; + site = 'https://wntl.net/'; + version = '1.0.9'; + filters: Filters | undefined = undefined; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + private async fetchNovels() { + const url = `${this.site}api/novels?page=1`; + const response = await fetchApi(url); + return response.json(); + } + + private getGenres(novels: any[]): string[] { + const genreSet = new Set<string>(); + novels.forEach((n: any) => + (n.genre || []).forEach((g: string) => genreSet.add(g)), + ); + return Array.from(genreSet).sort(); + } + + private buildFilters(novels: any[]): Filters { + const genres = this.getGenres(novels); + return { + genre: { + value: [], + label: 'Genre', + options: genres.map(g => ({ label: g, value: g })), + type: FilterTypes.Checkbox, + }, + }; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + const data = await this.fetchNovels(); + + if (!this.filters) { + this.filters = this.buildFilters(data.novels); + } + + let novels = data.novels; + + if (filters?.genre?.value?.length) { + novels = novels.filter((novel: any) => + (novel.genre || []).some((g: string) => + filters.genre.value.includes(g), + ), + ); + } + + return novels.map((novel: any) => ({ + name: novel.title, + cover: novel.cover ? this.site + novel.cover.slice(1) : defaultCover, + path: novel.id, + })); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const chaptersUrl = `${this.site}api/chapters/${novelPath}`; + const chaptersResponse = await fetchApi(chaptersUrl); + const chaptersData = await chaptersResponse.json(); + + const novelsUrl = `${this.site}api/novels?page=1`; + const novelsResponse = await fetchApi(novelsUrl); + const novelsData = await novelsResponse.json(); + + const novelData = novelsData.novels.find((n: any) => n.id === novelPath); + + const statusList = novelData?.status || []; + let novelStatus: NovelStatus; + if (statusList.includes('Completed')) { + novelStatus = NovelStatus.Completed; + } else if (statusList.includes('Ongoing')) { + novelStatus = NovelStatus.Ongoing; + } else if (statusList.includes('On-Break')) { + novelStatus = NovelStatus.OnHiatus; + } else { + novelStatus = NovelStatus.Unknown; + } + + const chapters: Plugin.ChapterItem[] = chaptersData.chapters.map( + (ch: any) => ({ + name: ch.title, + path: `${novelPath}/${ch.file}`, + releaseTime: ch.date, + chapterNumber: ch.number, + }), + ); + + return { + path: novelPath, + name: novelData?.title || 'Untitled', + cover: novelData?.cover + ? this.site + novelData.cover.slice(1) + : defaultCover, + author: novelData?.author || 'Unknown', + status: novelStatus, + genres: novelData?.genre?.join(', ') || '', + summary: novelData?.description || '', + chapters, + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = `${this.site}api/chapter-content/${chapterPath}`; + const response = await fetchApi(url); + const content = await response.text(); + + return content + .split('\n\n') + .map(p => `<p>${p}</p>`) + .join('\n'); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + if (!searchTerm || typeof searchTerm !== 'string') return []; + const data = await this.fetchNovels(); + + const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); + const searchLower = normalize(searchTerm); + if (!searchLower) return []; + + const filtered = data.novels.filter( + (novel: any) => + normalize(novel.title).includes(searchLower) || + (novel['alternate-title'] || []).some((alt: string) => + normalize(alt).includes(searchLower), + ), + ); + + return filtered.map((novel: any) => ({ + name: novel.title, + cover: novel.cover ? this.site + novel.cover.slice(1) : defaultCover, + path: novel.id, + })); + } + + resolveUrl = (path: string, isNovel?: boolean) => + isNovel + ? this.site + 'series/' + path + : this.site + 'read/' + path.replace(/\.md$/, ''); +} + +export default new WNTLPlugin(); diff --git a/plugins/english/wtrlab.ts b/plugins/english/wtrlab.ts new file mode 100644 index 000000000..5ef02c328 --- /dev/null +++ b/plugins/english/wtrlab.ts @@ -0,0 +1,1768 @@ +import { Plugin } from '@/types/plugin'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { gcm } from '@libs/aes'; + +class WTRLAB implements Plugin.PluginBase { + id = 'WTRLAB'; + name = 'WTR-LAB'; + site = 'https://wtr-lab.com/'; + version = '1.1.2'; + icon = 'src/en/wtrlab/icon.png'; + sourceLang = 'en/'; + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site + this.sourceLang + 'novel-list?'; + + const params = new URLSearchParams(); + params.append('orderBy', filters.orderBy.value); + params.append('order', filters.order.value); + params.append('status', filters.status.value); + params.append('release_status', filters.release_status.value); + params.append('addition_age', filters.addition_age.value); + params.append('page', page.toString()); + + if (filters.search.value) { + params.append('text', filters.search.value); + } + + if ( + filters.genres.value?.include && + filters.genres.value.include.length > 0 + ) { + params.append('gi', filters.genres.value.include.join(',')); + params.append('gc', filters.genre_operator.value); + } + if ( + filters.genres.value?.exclude && + filters.genres.value.exclude.length > 0 + ) { + params.append('ge', filters.genres.value.exclude.join(',')); + } + + if (filters.tags.value?.include && filters.tags.value.include.length > 0) { + params.append('ti', filters.tags.value.include.join(',')); + params.append('tc', filters.tag_operator.value); + } + if (filters.tags.value?.exclude && filters.tags.value.exclude.length > 0) { + params.append('te', filters.tags.value.exclude.join(',')); + } + + if (filters.folders.value) { + params.append('folders', filters.folders.value); + } + if (filters.library_exclude.value) { + params.append('le', filters.library_exclude.value); + } + + if (filters.min_chapters.value) { + params.append('minc', filters.min_chapters.value); + } + if (filters.min_rating.value) { + params.append('minr', filters.min_rating.value); + } + if (filters.min_review_count.value) { + params.append('minrc', filters.min_review_count.value); + } + + if (showLatestNovels) { + const response = await fetchApi(this.site + 'api/home/recent', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ page: page }), + }); + + const recentNovel: JsonNovel = await response.json(); + + // Parse novels from JSON + const novels: Plugin.NovelItem[] = recentNovel.data.map( + (datum: Datum) => ({ + name: datum.serie.data.title || datum.serie.slug || '', + cover: datum.serie.data.image, + path: + this.sourceLang + + 'serie-' + + datum.serie.raw_id + + '/' + + datum.serie.slug || '', + }), + ); + + return novels; + } else { + const finderPage = await fetchApi(this.site + 'en/novel-finder').then( + res => res.text(), + ); + const finderCheerio = parseHTML(finderPage); + const nextData = finderCheerio('#__NEXT_DATA__').html(); + if (!nextData) { + throw new Error('Could not find __NEXT_DATA__ on novel finder page'); + } + const buildId = JSON.parse(nextData).buildId; + + link = `${this.site}_next/data/${buildId}/en/novel-finder.json?${params.toString()}`; + + const response = await fetchApi(link); + const json = await response.json(); + const seenIds = new Set(); + + const novels: Plugin.NovelItem[] = json.pageProps.series + .filter((novel: Datum) => { + if (seenIds.has(novel.raw_id)) { + return false; + } + seenIds.add(novel.raw_id); + return true; + }) + .map((novel: Datum) => ({ + name: novel.data.title, + cover: novel.data.image, + path: `${this.sourceLang}serie-${novel.raw_id}/${novel.slug}`, + })); + + return novels; + } + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const nextDataElement = loadedCheerio('#__NEXT_DATA__'); + const nextDataText = nextDataElement.html(); + + let rawId: number | null = null; + let slug: string | null = null; + let chapterCount = 0; + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.text-uppercase').text(), + summary: loadedCheerio('.lead').text().trim(), + }; + + if (nextDataText) { + try { + const jsonData = JSON.parse(nextDataText); + const serieData = jsonData?.props?.pageProps?.serie?.serie_data; + + // console.log('Parsed novel JSON data:', serieData); + + if (serieData) { + novel.name = serieData.data?.title || ''; + novel.cover = serieData.data?.image || ''; + novel.summary = serieData.data?.description || ''; + novel.author = serieData.data?.author || ''; + rawId = serieData.raw_id || null; + slug = serieData.slug || null; + + switch (serieData.status) { + case 0: + novel.status = 'Ongoing'; + break; + case 1: + novel.status = 'Completed'; + break; + default: + novel.status = 'Unknown'; + } + } + } catch (error) { + console.error('Failed to parse __NEXT_DATA__:', error); + } + } + + if (!novel.name) { + novel.name = + loadedCheerio('h1.text-uppercase').text() || + loadedCheerio('h1.long-title').text() || + loadedCheerio('.title-wrap h1').text().trim(); + } + + if (!novel.cover) { + novel.cover = + loadedCheerio('.image-wrap img').attr('src') || + loadedCheerio('.img-wrap > img').attr('src'); + } + + if (!novel.summary) { + novel.summary = + loadedCheerio('.description').text().trim() || + loadedCheerio('.desc-wrap .description').text().trim() || + loadedCheerio('.lead').text().trim(); + } + + const genres = + loadedCheerio('td:contains("Genre")') + .next() + .find('a') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .trim(), + ) + .toArray() || + loadedCheerio('.genre') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .trim(), + ) + .toArray() || + loadedCheerio('.genres .genre') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .trim(), + ) + .toArray(); + + if (genres.length > 0) { + novel.genres = genres + .map(g => g.replace(/,$/, '').trim()) + .filter(genre => genre && genre.length > 0) + .join(', '); + } + + const tags = + loadedCheerio('td:contains("Tags")') + .next() + .find('a') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .replace(/,$/, '') + .trim(), + ) + .toArray() || + loadedCheerio('.tag') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .replace(/,$/, '') + .trim(), + ) + .toArray() || + loadedCheerio('.tags .tag') + .map((i, el) => + loadedCheerio(el) + .text() + .replace(/<!--.*?-->/g, '') + .replace(/,$/, '') + .trim(), + ) + .toArray(); + + // console.log('Found tags from HTML:', tags); + + if (tags.length > 0) { + const existingGenres = novel.genres ? novel.genres.split(', ') : []; + // console.log('Existing genres:', existingGenres); + const allGenres = [...existingGenres, ...tags].filter( + genre => genre && genre.length > 0, + ); + const uniqueGenres = allGenres.filter( + (genre, index) => allGenres.indexOf(genre) === index, + ); + novel.genres = uniqueGenres.join(', '); + // console.log('Combined genres:', novel.genres); + } + + if (!novel.author) { + novel.author = + loadedCheerio('td:contains("Author")') + .next() + .text() + .replace(/[\t\n]/g, '') + .trim() || + loadedCheerio('td:contains("Author") + td') + .text() + .replace(/[\t\n]/g, '') + .trim(); + } + + if (!novel.status) { + novel.status = + loadedCheerio('td:contains("Status")') + .next() + .text() + .replace(/[\t\n]/g, '') + .trim() || + loadedCheerio('td:contains("Status") + td') + .text() + .replace(/[\t\n]/g, '') + .trim() || + loadedCheerio('.detail-line:contains("•")') + .text() + .match(/•\s*(\w+)/)?.[1] || + ''; + } + + const urlMatch = novelPath.match(/serie-(\d+)\/([^/]+)/); + if (urlMatch) { + rawId = parseInt(urlMatch[1]); + slug = urlMatch[2]; + } + + const chapterCountText = + loadedCheerio('.detail-line:contains("Chapters")').text() || + loadedCheerio('div:contains("Chapters")').text(); + const chapterCountMatch = chapterCountText.match(/(\d+)\s+Chapters?/i); + if (chapterCountMatch) { + chapterCount = parseInt(chapterCountMatch[1]); + } + + let chapters: Plugin.ChapterItem[] = []; + + if (rawId && slug && chapterCount > 0) { + try { + chapters = await this.fetchAllChapters(rawId, chapterCount, slug); + } catch (error) { + console.error('Failed to fetch chapters via API:', error); + chapters = []; + } + } else { + console.warn('Could not extract rawId, slug, or chapterCount from page', { + rawId, + slug, + chapterCount, + }); + } + + novel.chapters = chapters; + + return novel; + } + + async decrypt(encrypted: string, encKey: string) { + try { + // t is set to false here; true if arr: + // If true we parse as json + let t = !1, + u = encrypted; + // t true if arr:, str: straight, else error + encrypted.startsWith('arr:') + ? ((t = !0), (u = encrypted.substring(4))) + : encrypted.startsWith('str:') && (u = encrypted.substring(4)); + const r = u.split(':'); + if (3 !== r.length) throw Error('Invalid encrypted data format'); + + // Remove base64, setup vars + const [iv, tag, ciphertext] = r.map(part => + Uint8Array.from(atob(part), e => e.charCodeAt(0)), + ), + combined = new Uint8Array(ciphertext.length + tag.length); + + // Make the ciphertext + tag format expected for decryption + combined.set(ciphertext), combined.set(tag, ciphertext.length); + + // Decrypt with encKey + // Convert the key to bytes (first 32 characters of encKey) + const keyBytes = new TextEncoder().encode(encKey.slice(0, 32)); + + // Create AES-GCM cipher instance + const aes = gcm(keyBytes, iv); + + // Decrypt the combined ciphertext + const decrypted = aes.decrypt(combined); + + // Convert decrypted bytes to string + const m = new TextDecoder().decode(decrypted); + + // const D = new TextEncoder().encode(encKey.slice(0, 32)); + // const d = await crypto.subtle.importKey( + // 'raw', + // D, + // { name: 'AES-GCM' }, + // !1, + // ['decrypt'], + // ); + // const h = await crypto.subtle.decrypt( + // { name: 'AES-GCM', iv: iv }, + // d, + // combined, + // ); + // const m = new TextDecoder().decode(h); + + // If it was arr:, parse as json + if (t) return JSON.parse(m); + // Otherwise (str:) return straight + return m; + } catch (error) { + console.error('Client-side decryption error:', error); + const msg = { 'error': `<p>Client-side decryption error:</p>${error}` }; + return msg; + } + } + + async getKey($: CheerioAPI): Promise<string> { + // Fetch the novel's data in JSON format + const searchKey = 'TextEncoder().encode("'; + + const URLs: string[] = []; + let code: string | undefined; + let index = -1; + + // Find URL with API Key + const scripts = $('head').find('script').toArray(); + for (const el of scripts) { + const src = $(el).attr('src'); + if (!src) continue; + if (URLs.includes(src)) continue; + URLs.push(src); + } + + for (const src of URLs) { + const script = await fetchApi(`${this.site}${src}`); + const raw = await script.text(); + index = raw.indexOf(searchKey); + if (index >= 0) { + code = raw; + break; + } + } + if (!code) { + throw new Error('Failed to find Encryption Key'); + } + // Get right segment of code + const encKey = code.substring(index + 22, index + 54); + return encKey; + } + + async translate(data: string[]): Promise<string[]> { + const contained = data.map((line, i) => `<a i=${i}>${line}</a>`); + + const response = await fetchApi( + 'https://translate-pa.googleapis.com/v1/translateHtml', + { + 'credentials': 'omit', + 'headers': { + 'content-type': 'application/json+protobuf', + // Generic public API key source also uses + // Seen all over google + 'X-Goog-API-Key': 'AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520', + }, + 'referrer': 'https://wtr-lab.com/', + 'body': `[[${JSON.stringify(contained)},"zh-CN","en"],"te_lib"]`, + 'method': 'POST', + }, + ); + const translated = await response.json(); + const out = translated && translated[0] ? translated[0] : []; + return out as string[]; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + let rawId: number | null = null; + let chapterNo: number | null = null; + let loadedCheerio = null; + + const urlMatch = chapterPath.match(/serie-(\d+)\/[^/]+\/chapter-(\d+)/); + if (urlMatch) { + rawId = parseInt(urlMatch[1], 10); + chapterNo = parseInt(urlMatch[2], 10); + // console.log('Extracted from URL - rawId:', rawId, 'chapterNo:', chapterNo); + } + + if (!rawId || !chapterNo) { + const body = await fetchApi(url).then(res => res.text()); + + loadedCheerio = parseHTML(body); + const chapterJson = loadedCheerio('#__NEXT_DATA__').html() + ''; + const jsonData: NovelJson = JSON.parse(chapterJson); + + // const chapterID = jsonData.props.pageProps.serie.chapter.id; + rawId = jsonData.props.pageProps.serie.chapter.raw_id; + chapterNo = jsonData.props.pageProps.serie.chapter.order; + } + + if (!rawId || !chapterNo) { + const errorMsg = `Missing required parameters for API call from URL '${chapterPath}' - rawId: ${rawId}, chapterNo: ${chapterNo}. Please check the URL format.`; + console.error(errorMsg); + throw new Error(errorMsg); + } + + const translationTypes = ['ai', 'web']; + + let eLog = ''; + let parsedJson; + + for (const type of translationTypes) { + const apiResponse = await fetchApi(`${this.site}api/reader/get`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + referrer: url, + body: JSON.stringify({ + translate: type, + language: this.sourceLang.replace('/', ''), + raw_id: rawId, + chapter_no: chapterNo, + retry: false, + force_retry: false, + }), + }); + + parsedJson = await apiResponse.json(); + if (!apiResponse.ok) { + if (parsedJson.error) { + eLog = parsedJson.error; + continue; + } + } else if (!parsedJson.error) { + break; + } + } + if (parsedJson.success == false) { + const errorMsg = parsedJson.message; + console.error(errorMsg); + throw new Error(errorMsg); + } + let chapterContent = parsedJson.data.data.body; + const chapterGlossary: ChapterContent['glossary_data'] | undefined = + parsedJson?.data?.data?.glossary_data; + + let htmlString = ''; + + if ( + chapterContent.toString().startsWith('arr:') || + chapterContent.toString().startsWith('str:') + ) { + if (!loadedCheerio) { + const body = await fetchApi(url).then(res => res.text()); + + loadedCheerio = parseHTML(body); + } + const encKey = await this.getKey(loadedCheerio); + chapterContent = await this.decrypt(chapterContent, encKey); + if (Object.prototype.hasOwnProperty.call(chapterContent, 'error')) { + htmlString += `<p>${chapterContent.error.toString()}</p>`; + return htmlString; + } + chapterContent = await this.translate(chapterContent); + htmlString += `<p><small>This is being translated from your device via google translate (source's method) - Login via web view to try for ai translations</small></p>`; + } + + if (eLog !== '') { + htmlString += `<p style="color:darkred;">${eLog}</p>`; + } + + const dictionary = chapterGlossary?.terms?.map(t => t[0]) || []; + + for (let text of chapterContent) { + if (dictionary.length > 0) { + text = text.replaceAll( + /(?:wtr-lab\s+)?※([0-9]+)[⛬〓]/g, + (m: string, index: string) => dictionary[parseInt(index)] || m, + ); + } + htmlString += `<p>${text}</p>`; + } + + return htmlString; + } + + async fetchAllChapters( + rawId: number, + totalChapters: number, + slug: string, + ): Promise<Plugin.ChapterItem[]> { + const allChapters: Plugin.ChapterItem[] = []; + const batchSize = 250; + + for (let start = 1; start <= totalChapters; start += batchSize) { + const end = Math.min(start + batchSize - 1, totalChapters); + + try { + const response = await fetchApi( + `${this.site}api/chapters/${rawId}?start=${start}&end=${end}`, + ); + + const data = await response.json(); + + if (data.chapters && Array.isArray(data.chapters)) { + const batchChapters: Plugin.ChapterItem[] = data.chapters.map( + (apiChapter: ApiChapter) => ({ + name: apiChapter.title, + path: `${this.sourceLang}serie-${rawId}/${slug}/chapter-${apiChapter.order}`, + releaseTime: apiChapter.updated_at?.substring(0, 10), + chapterNumber: apiChapter.order, + }), + ); + + allChapters.push(...batchChapters); + } + + if (!data.chapters || data.chapters.length < batchSize) { + break; + } + } catch (error) { + console.error(`Failed to fetch chapters ${start}-${end}:`, error); + continue; + } + } + + return allChapters.sort( + (a, b) => (a.chapterNumber || 0) - (b.chapterNumber || 0), + ); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const filters = this.filters; + filters.search.value = searchTerm; + return this.popularNovels(page, { showLatestNovels: false, filters }); + } + + filters = { + search: { + value: '', + label: 'Search', + type: FilterTypes.TextInput, + }, + orderBy: { + value: 'update', + label: 'Order by', + options: [ + { label: 'Update Date', value: 'update' }, + { label: 'Addition Date', value: 'date' }, + { label: 'Random', value: 'random' }, + { label: 'Weekly View', value: 'weekly_rank' }, + { label: 'Monthly View', value: 'monthly_rank' }, + { label: 'All-Time View', value: 'view' }, + { label: 'Name', value: 'name' }, + { label: 'Reader', value: 'reader' }, + { label: 'Chapter', value: 'chapter' }, + { label: 'Rating', value: 'rating' }, + { label: 'Review Count', value: 'total_rate' }, + { label: 'Vote Count', value: 'vote' }, + ], + type: FilterTypes.Picker, + }, + order: { + value: 'desc', + label: 'Order', + options: [ + { label: 'Descending', value: 'desc' }, + { label: 'Ascending', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + status: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + { label: 'Hiatus', value: 'hiatus' }, + { label: 'Dropped', value: 'dropped' }, + ], + type: FilterTypes.Picker, + }, + release_status: { + value: 'all', + label: 'Release Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Released', value: 'released' }, + { label: 'On Voting', value: 'voting' }, + ], + type: FilterTypes.Picker, + }, + addition_age: { + value: 'all', + label: 'Addition Age', + options: [ + { label: 'All', value: 'all' }, + { label: '< 2 Days', value: 'day' }, + { label: '< 1 Week', value: 'week' }, + { label: '< 1 Month', value: 'month' }, + ], + type: FilterTypes.Picker, + }, + min_chapters: { + value: '', + label: 'Minimum Chapters', + type: FilterTypes.TextInput, + }, + min_rating: { + value: '', + label: 'Minimum Rating (0.0-5.0)', + type: FilterTypes.TextInput, + }, + min_review_count: { + value: '', + label: 'Minimum Review Count', + type: FilterTypes.TextInput, + }, + genre_operator: { + value: 'and', + label: 'Genre (And/Or)', + options: [ + { label: 'And', value: 'and' }, + { label: 'Or', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Genres', + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Male Protagonist', value: '417' }, + { label: 'Transmigration', value: '717' }, + { label: 'System', value: '696' }, + { label: 'Cultivation', value: '169' }, + { label: 'Special Abilities', value: '667' }, + { label: 'Female Protagonist', value: '275' }, + { label: 'Fanfiction', value: '263' }, + { label: 'Weak to Strong', value: '750' }, + { label: 'Handsome Male Lead', value: '327' }, + { label: 'Beautiful Female Lead', value: '81' }, + { label: 'Game Elements', value: '297' }, + { label: 'Cheats', value: '122' }, + { label: 'Genius Protagonist', value: '306' }, + { label: 'Reincarnation', value: '578' }, + { label: 'Harem-seeking Protagonist', value: '329' }, + { label: 'Time Travel', value: '710' }, + { label: 'Overpowered Protagonist', value: '506' }, + { label: 'Modern Day', value: '446' }, + { label: 'Business Management', value: '108' }, + { label: 'Calm Protagonist', value: '111' }, + { label: 'Magic', value: '410' }, + { label: 'Immortals', value: '357' }, + { label: 'Clever Protagonist', value: '134' }, + { label: 'Ruthless Protagonist', value: '595' }, + { label: 'Apocalypse', value: '47' }, + { label: 'World Hopping', value: '756' }, + { label: 'Poor to Rich', value: '540' }, + { label: 'Douluo Dalu', value: '772' }, + { label: 'Naruto', value: '769' }, + { label: 'Farming', value: '266' }, + { label: 'Fantasy World', value: '265' }, + { label: 'Kingdom Building', value: '379' }, + { label: 'Fast Cultivation', value: '267' }, + { label: 'Protagonist Strong from the Start', value: '560' }, + { label: 'Cunning Protagonist', value: '171' }, + { label: 'Nationalism', value: '476' }, + { label: 'Schemes And Conspiracies', value: '601' }, + { label: 'Survival', value: '692' }, + { label: 'Post-apocalyptic', value: '544' }, + { label: 'Hard-Working Protagonist', value: '328' }, + { label: 'Showbiz', value: '640' }, + { label: 'Unlimited Flow', value: '735' }, + { label: 'Demons', value: '191' }, + { label: 'Monsters', value: '452' }, + { label: 'Dragons', value: '216' }, + { label: 'Romantic Subplot', value: '592' }, + { label: 'Polygamy', value: '538' }, + { label: 'Beast Companions', value: '78' }, + { label: 'Marvel', value: '766' }, + { label: 'Evolution', value: '248' }, + { label: 'One Piece', value: '767' }, + { label: 'Leadership', value: '388' }, + { label: 'Alternate World', value: '30' }, + { label: 'Pets', value: '520' }, + { label: 'World Travel', value: '757' }, + { label: 'Celebrities', value: '117' }, + { label: 'Strong to Stronger', value: '682' }, + { label: 'Game Ranking System', value: '298' }, + { label: 'Alchemy', value: '27' }, + { label: 'Arrogant Characters', value: '56' }, + { label: 'Multiple Realms', value: '459' }, + { label: 'Army Building', value: '54' }, + { label: 'Magical Space', value: '414' }, + { label: 'Wealthy Characters', value: '751' }, + { label: 'Early Romance', value: '225' }, + { label: 'Racism', value: '570' }, + { label: 'Devoted Love Interests', value: '198' }, + { label: 'Comedic Undertone', value: '146' }, + { label: 'Businessmen', value: '109' }, + { label: 'Second Chance', value: '606' }, + { label: 'Revenge', value: '585' }, + { label: 'Wizards', value: '755' }, + { label: 'Pregnancy', value: '549' }, + { label: 'Ancient China', value: '34' }, + { label: 'Black Belly', value: '87' }, + { label: 'Evil Protagonist', value: '246' }, + { label: 'Love Interest Falls in Love First', value: '403' }, + { label: 'Evil Gods', value: '244' }, + { label: 'Academy', value: '5' }, + { label: 'Outer Space', value: '505' }, + { label: 'Zombies', value: '765' }, + { label: 'Single Female Lead', value: '787' }, + { label: 'Mythology', value: '473' }, + { label: 'Gods', value: '316' }, + { label: 'Harry Potter', value: '768' }, + { label: 'Sword Wielder', value: '695' }, + { label: 'Shameless Protagonist', value: '630' }, + { label: 'Futuristic Setting', value: '294' }, + { label: 'Pokemon', value: '771' }, + { label: 'Parallel Worlds', value: '510' }, + { label: 'Level System', value: '390' }, + { label: 'Beasts', value: '80' }, + { label: 'Strong Love Interests', value: '681' }, + { label: 'Fantasy Creatures', value: '264' }, + { label: 'Modern Knowledge', value: '447' }, + { label: 'Hiding True Identity', value: '343' }, + { label: 'Loyal Subordinates', value: '408' }, + { label: 'Slow Romance', value: '659' }, + { label: 'Family', value: '257' }, + { label: 'Politics', value: '536' }, + { label: 'Determined Protagonist', value: '197' }, + { label: 'Hiding True Abilities', value: '342' }, + { label: 'Cosmic Wars', value: '156' }, + { label: 'Ancient Times', value: '35' }, + { label: 'Arranged Marriage', value: '55' }, + { label: 'Complex Family Relationships', value: '148' }, + { label: 'Cold Protagonist', value: '142' }, + { label: 'Ghosts', value: '307' }, + { label: 'Sword And Magic', value: '694' }, + { label: 'Based on an Anime', value: '74' }, + { label: 'Wars', value: '748' }, + { label: 'Survival Game', value: '693' }, + { label: 'Military', value: '437' }, + { label: 'Betrayal', value: '83' }, + { label: 'Misunderstandings', value: '442' }, + { label: 'Time Skip', value: '709' }, + { label: 'Bloodlines', value: '93' }, + { label: 'Transported to Another World', value: '721' }, + { label: 'Cautious Protagonist', value: '116' }, + { label: 'Nobles', value: '485' }, + { label: 'Technological Gap', value: '699' }, + { label: 'Doting Love Interests', value: '211' }, + { label: 'Antihero Protagonist', value: '43' }, + { label: 'Godly Powers', value: '315' }, + { label: 'Reincarnated in Another World', value: '577' }, + { label: 'Lucky Protagonist', value: '409' }, + { label: 'Virtual Reality', value: '742' }, + { label: 'Medical Knowledge', value: '433' }, + { label: 'God Protagonist', value: '312' }, + { label: 'Adapted to Manhua', value: '15' }, + { label: 'Fast Learner', value: '268' }, + { label: 'Childcare', value: '126' }, + { label: 'Kingdoms', value: '380' }, + { label: 'Scientists', value: '603' }, + { label: 'Underestimated Protagonist', value: '731' }, + { label: 'Multiple Identities', value: '455' }, + { label: 'Naive Protagonist', value: '474' }, + { label: 'Doctors', value: '208' }, + { label: 'Artifacts', value: '58' }, + { label: 'Older Love Interests', value: '492' }, + { label: 'Elves', value: '233' }, + { label: 'Hidden Abilities', value: '341' }, + { label: 'Power Couple', value: '545' }, + { label: 'Cooking', value: '154' }, + { label: 'Unique Cultivation Technique', value: '732' }, + { label: 'Body Tempering', value: '95' }, + { label: 'Chat Rooms', value: '121' }, + { label: 'Eye Powers', value: '251' }, + { label: 'Artificial Intelligence', value: '59' }, + { label: 'Master-Disciple Relationship', value: '428' }, + { label: 'Interdimensional Travel', value: '368' }, + { label: 'Famous Protagonist', value: '261' }, + { label: 'Royalty', value: '594' }, + { label: 'Low-key Protagonist', value: '407' }, + { label: 'Late Romance', value: '385' }, + { label: 'Gamers', value: '299' }, + { label: 'Monster Tamer', value: '451' }, + { label: 'Possessive Characters', value: '543' }, + { label: 'Aliens', value: '28' }, + { label: 'Multiple POV', value: '457' }, + { label: 'Mythical Beasts', value: '472' }, + { label: 'Familial Love', value: '255' }, + { label: 'Confident Protagonist', value: '150' }, + { label: 'Mature Protagonist', value: '432' }, + { label: 'Rape', value: '571' }, + { label: 'Reincarnated as a Monster', value: '574' }, + { label: 'Slow Growth at Start', value: '658' }, + { label: 'Cold Love Interests', value: '141' }, + { label: 'Character Growth', value: '118' }, + { label: 'Sect Development', value: '613' }, + { label: 'Summoning Magic', value: '691' }, + { label: 'Acting', value: '7' }, + { label: 'Ability Steal', value: '2' }, + { label: 'Movies', value: '453' }, + { label: 'Ninjas', value: '484' }, + { label: 'Previous Life Talent', value: '551' }, + { label: 'Gate to Another World', value: '301' }, + { label: 'Money Grubber', value: '448' }, + { label: 'Non-humanoid Protagonist', value: '486' }, + { label: 'Dark', value: '181' }, + { label: 'Strength-based Social Hierarchy', value: '680' }, + { label: 'Industrialization', value: '362' }, + { label: 'Mysterious Past', value: '470' }, + { label: 'Caring Protagonist', value: '115' }, + { label: 'Pirates', value: '529' }, + { label: 'Pill Concocting', value: '527' }, + { label: 'European Ambience', value: '243' }, + { label: 'Cruel Characters', value: '167' }, + { label: 'Charismatic Protagonist', value: '119' }, + { label: 'Strategist', value: '679' }, + { label: 'Assassins', value: '61' }, + { label: 'Secret Organizations', value: '609' }, + { label: 'Knights', value: '381' }, + { label: 'Vampires', value: '740' }, + { label: 'Firearms', value: '278' }, + { label: 'Army', value: '53' }, + { label: 'Dao Comprehension', value: '179' }, + { label: 'Absent Parents', value: '3' }, + { label: 'Clan Building', value: '132' }, + { label: 'Detectives', value: '196' }, + { label: 'Heroes', value: '339' }, + { label: 'Friendship', value: '291' }, + { label: 'Charming Protagonist', value: '120' }, + { label: 'Accelerated Growth', value: '6' }, + { label: 'College/University', value: '144' }, + { label: 'Depictions of Cruelty', value: '193' }, + { label: 'Artifact Crafting', value: '57' }, + { label: 'Doting Parents', value: '213' }, + { label: 'Past Plays a Big Role', value: '515' }, + { label: 'MMORPG', value: '443' }, + { label: 'Card Games', value: '113' }, + { label: 'Magic Beasts', value: '411' }, + { label: 'Tragic Past', value: '715' }, + { label: 'First-time Intercourse', value: '280' }, + { label: 'Transported into a Game World', value: '719' }, + { label: 'Mysterious Family Background', value: '468' }, + { label: 'Management', value: '420' }, + { label: 'Secret Identity', value: '608' }, + { label: 'Earth Invasion', value: '226' }, + { label: 'Clones', value: '136' }, + { label: 'Based on a Video Game', value: '72' }, + { label: 'Swallowed Star', value: '785' }, + { label: 'Magic Formations', value: '412' }, + { label: 'Gao Wu', value: '781' }, + { label: 'Genetic Modifications', value: '304' }, + { label: 'Male Yandere', value: '419' }, + { label: 'Writers', value: '759' }, + { label: 'Based on a Movie', value: '69' }, + { label: 'Elemental Magic', value: '232' }, + { label: 'Discrimination', value: '201' }, + { label: 'Marriage', value: '424' }, + { label: 'Evil Organizations', value: '245' }, + { label: 'Younger Sisters', value: '764' }, + { label: 'Sudden Wealth', value: '688' }, + { label: 'Doting Older Siblings', value: '212' }, + { label: 'Cute Children', value: '174' }, + { label: 'Manipulative Characters', value: '422' }, + { label: 'Age Progression', value: '24' }, + { label: 'Hunters', value: '353' }, + { label: 'Adventurers', value: '22' }, + { label: 'Threesome', value: '704' }, + { label: 'Mystery Solving', value: '471' }, + { label: 'Perverted Protagonist', value: '519' }, + { label: 'Jack of All Trades', value: '372' }, + { label: 'Battle Competition', value: '76' }, + { label: 'Multiple Reincarnated Individuals', value: '460' }, + { label: 'Sex Slaves', value: '627' }, + { label: 'Soul Power', value: '663' }, + { label: 'Orphans', value: '500' }, + { label: 'Martial Spirits', value: '426' }, + { label: 'Dense Protagonist', value: '192' }, + { label: 'Family Conflict', value: '259' }, + { label: 'Magical Technology', value: '415' }, + { label: 'Warhammer', value: '775' }, + { label: 'Smart Couple', value: '660' }, + { label: 'Teachers', value: '697' }, + { label: 'Police', value: '534' }, + { label: 'Selfish Protagonist', value: '616' }, + { label: 'Simulator', value: '786' }, + { label: 'Demonic Cultivation Technique', value: '190' }, + { label: 'Rape Victim Becomes Lover', value: '572' }, + { label: 'Hackers', value: '324' }, + { label: 'Sudden Strength Gain', value: '687' }, + { label: 'Imperial Harem', value: '358' }, + { label: 'Family Business', value: '258' }, + { label: 'Cute Protagonist', value: '175' }, + { label: 'Apathetic Protagonist', value: '46' }, + { label: 'Lack of Common Sense', value: '383' }, + { label: 'Aristocracy', value: '51' }, + { label: 'Death of Loved Ones', value: '184' }, + { label: 'Enemies Become Lovers', value: '237' }, + { label: 'Empires', value: '235' }, + { label: 'Dungeons', value: '221' }, + { label: 'Male to Female', value: '418' }, + { label: 'Lazy Protagonist', value: '387' }, + { label: 'Evil Religions', value: '247' }, + { label: 'Obsessive Love', value: '490' }, + { label: 'Easy Going Life', value: '227' }, + { label: 'Appearance Changes', value: '48' }, + { label: 'Demon Lord', value: '189' }, + { label: 'Carefree Protagonist', value: '114' }, + { label: 'Mutations', value: '466' }, + { label: 'Student-Teacher Relationship', value: '685' }, + { label: 'R-18', value: '568' }, + { label: 'Abusive Characters', value: '4' }, + { label: 'Appearance Different from Actual Age', value: '49' }, + { label: 'Football', value: '780' }, + { label: 'Human-Nonhuman Relationship', value: '351' }, + { label: 'Pragmatic Protagonist', value: '547' }, + { label: 'Hot-blooded Protagonist', value: '348' }, + { label: 'Necromancer', value: '478' }, + { label: 'Battle Academy', value: '75' }, + { label: 'Witches', value: '754' }, + { label: 'Yandere', value: '760' }, + { label: 'Dragon Ball', value: '773' }, + { label: 'Childhood Friends', value: '127' }, + { label: 'Based on a TV Show', value: '71' }, + { label: 'Dwarfs', value: '222' }, + { label: 'Inheritance', value: '364' }, + { label: 'Child Protagonist', value: '125' }, + { label: 'Honkai', value: '818' }, + { label: 'Daoism', value: '180' }, + { label: 'Heavenly Tribulation', value: '335' }, + { label: 'Netori', value: '482' }, + { label: 'Sexual Cultivation Technique', value: '629' }, + { label: 'Buddhism', value: '106' }, + { label: 'Broken Engagement', value: '103' }, + { label: 'Reverse Rape', value: '587' }, + { label: 'Time Manipulation', value: '707' }, + { label: 'DC Universe', value: '778' }, + { label: 'Eidetic Memory', value: '230' }, + { label: 'Clingy Lover', value: '135' }, + { label: 'Live Streaming', value: '782' }, + { label: 'Mutated Creatures', value: '465' }, + { label: 'Phoenixes', value: '524' }, + { label: 'Sharp-tongued Characters', value: '633' }, + { label: 'Souls', value: '664' }, + { label: 'Poor Protagonist', value: '539' }, + { label: 'Angels', value: '38' }, + { label: 'Singers', value: '648' }, + { label: 'Proactive Protagonist', value: '555' }, + { label: 'Heartwarming', value: '333' }, + { label: 'Fellatio', value: '273' }, + { label: 'Spatial Manipulation', value: '665' }, + { label: 'Tsundere', value: '725' }, + { label: 'Enemies Become Allies', value: '236' }, + { label: 'e-Sports', value: '224' }, + { label: 'Mind Control', value: '439' }, + { label: 'Mercenaries', value: '435' }, + { label: 'Adopted Protagonist', value: '20' }, + { label: 'Average-looking Protagonist', value: '65' }, + { label: 'Master-Servant Relationship', value: '429' }, + { label: 'Gore', value: '318' }, + { label: 'Store Owner', value: '675' }, + { label: 'Amnesia', value: '31' }, + { label: 'Human Experimentation', value: '349' }, + { label: 'Strategic Battles', value: '678' }, + { label: 'Goddesses', value: '314' }, + { label: 'Skill Assimilation', value: '651' }, + { label: 'Abandoned Children', value: '1' }, + { label: 'Bleach', value: '770' }, + { label: 'Death', value: '183' }, + { label: 'Emotionally Weak Protagonist', value: '234' }, + { label: 'Aggressive Characters', value: '26' }, + { label: 'Resurrection', value: '583' }, + { label: 'Cross-dressing', value: '165' }, + { label: 'Transformation Ability', value: '716' }, + { label: 'Villainess Noble Girls', value: '741' }, + { label: 'Insects', value: '366' }, + { label: 'Thriller', value: '705' }, + { label: 'Orcs', value: '497' }, + { label: 'Boss-Subordinate Relationship', value: '100' }, + { label: 'Fated Lovers', value: '271' }, + { label: 'Music', value: '464' }, + { label: 'Economics', value: '228' }, + { label: 'Loli', value: '395' }, + { label: 'Couple Growth', value: '158' }, + { label: 'Incest', value: '359' }, + { label: 'Multiple Transported Individuals', value: '462' }, + { label: 'Protagonist with Multiple Bodies', value: '561' }, + { label: 'Religions', value: '579' }, + { label: 'Game Creator', value: '784' }, + { label: 'Soldiers', value: '662' }, + { label: 'Righteous Protagonist', value: '590' }, + { label: 'Blacksmith', value: '89' }, + { label: 'Adopted Children', value: '19' }, + { label: 'Yu-Gi-Oh!', value: '774' }, + { label: 'Twins', value: '726' }, + { label: 'Crossover', value: '166' }, + { label: 'Power Struggle', value: '546' }, + { label: 'Otaku', value: '501' }, + { label: 'Saints', value: '597' }, + { label: 'Teamwork', value: '698' }, + { label: 'Age Regression', value: '25' }, + { label: 'Honghuang', value: '801' }, + { label: 'Siblings Not Related by Blood', value: '645' }, + { label: 'Reincarnated in a Game World', value: '576' }, + { label: 'Poisons', value: '533' }, + { label: 'Fox Spirits', value: '289' }, + { label: 'Adapted to Manga', value: '14' }, + { label: 'Sexual Abuse', value: '628' }, + { label: 'Dolls/Puppets', value: '209' }, + { label: 'Long Separations', value: '398' }, + { label: 'Proficiency', value: '793' }, + { label: 'Skill Creation', value: '653' }, + { label: 'Gangs', value: '300' }, + { label: 'Gunfighters', value: '323' }, + { label: 'Journey to the West', value: '796' }, + { label: 'Detective Conan', value: '804' }, + { label: 'Popular Love Interests', value: '541' }, + { label: 'Pill Based Cultivation', value: '526' }, + { label: 'Destiny', value: '195' }, + { label: 'Parody', value: '513' }, + { label: 'Multiple Timelines', value: '461' }, + { label: 'Personality Changes', value: '518' }, + { label: 'Psychic Powers', value: '562' }, + { label: 'Generals', value: '303' }, + { label: 'Narcissistic Protagonist', value: '475' }, + { label: 'Transplanted Memories', value: '718' }, + { label: 'Crime', value: '163' }, + { label: 'Domestic Affairs', value: '210' }, + { label: 'Murders', value: '463' }, + { label: 'Guilds', value: '322' }, + { label: 'Books', value: '98' }, + { label: 'Chefs', value: '123' }, + { label: 'Mortal Flow', value: '792' }, + { label: 'Loner Protagonist', value: '397' }, + { label: 'Contracts', value: '153' }, + { label: 'Quirky Characters', value: '566' }, + { label: 'Adapted to Anime', value: '10' }, + { label: 'Beastkin', value: '79' }, + { label: 'Archery', value: '50' }, + { label: 'Adultery', value: '21' }, + { label: 'Harsh Training', value: '330' }, + { label: 'Organized Crime', value: '498' }, + { label: 'Biochip', value: '85' }, + { label: 'Fairies', value: '252' }, + { label: 'Psychopaths', value: '563' }, + { label: 'Multiple Protagonists', value: '458' }, + { label: 'Ugly to Beautiful', value: '729' }, + { label: 'Playful Protagonist', value: '531' }, + { label: 'Minecraft', value: '790' }, + { label: 'Medieval', value: '434' }, + { label: 'Divination', value: '205' }, + { label: 'Younger Love Interests', value: '763' }, + { label: 'Sister Complex', value: '650' }, + { label: 'Maids', value: '416' }, + { label: 'Protagonist Falls in Love First', value: '559' }, + { label: 'Dreams', value: '217' }, + { label: 'Persistent Love Interests', value: '517' }, + { label: 'Hunter x Hunter', value: '777' }, + { label: 'Brother Complex', value: '104' }, + { label: 'Humanoid Protagonist', value: '352' }, + { label: 'Brotherhood', value: '105' }, + { label: 'Playboys', value: '530' }, + { label: 'Jealousy', value: '373' }, + { label: 'Tribal Society', value: '723' }, + { label: 'Secrets', value: '612' }, + { label: 'Saving the World', value: '600' }, + { label: 'Slaves', value: '656' }, + { label: 'Three Kingdoms', value: '795' }, + { label: 'Childhood Love', value: '128' }, + { label: 'Thieves', value: '703' }, + { label: 'Demi-Humans', value: '188' }, + { label: 'Dao Companion', value: '178' }, + { label: 'Sign In', value: '811' }, + { label: 'Race Change', value: '569' }, + { label: 'Crafting', value: '162' }, + { label: 'First Love', value: '279' }, + { label: 'Cyberpunk 2077', value: '783' }, + { label: 'Curses', value: '173' }, + { label: 'Spirit Advisor', value: '669' }, + { label: 'Marriage of Convenience', value: '425' }, + { label: 'Near-Death Experience', value: '477' }, + { label: 'Lost Civilizations', value: '400' }, + { label: 'Prophecies', value: '557' }, + { label: 'Forced Marriage', value: '286' }, + { label: 'Episodic', value: '241' }, + { label: 'Conferred Gods', value: '800' }, + { label: 'Artists', value: '60' }, + { label: 'Animal Characteristics', value: '39' }, + { label: 'Cannibalism', value: '112' }, + { label: 'Fearless Protagonist', value: '272' }, + { label: 'Dark Fantasy', value: '789' }, + { label: 'Secretive Protagonist', value: '611' }, + { label: 'God-human Relationship', value: '313' }, + { label: 'Child Abuse', value: '124' }, + { label: 'Cowardly Protagonist', value: '161' }, + { label: 'Anti-social Protagonist', value: '42' }, + { label: 'Prison', value: '554' }, + { label: 'Female Master', value: '274' }, + { label: 'Hollywood', value: '779' }, + { label: 'Past Trauma', value: '516' }, + { label: 'Torture', value: '713' }, + { label: 'Adapted to Drama', value: '11' }, + { label: 'Bullying', value: '107' }, + { label: 'Androgynous Characters', value: '36' }, + { label: 'Class Awakening', value: '827' }, + { label: 'Multiple Personalities', value: '456' }, + { label: 'Corruption', value: '155' }, + { label: 'Merchants', value: '436' }, + { label: 'Animal Rearing', value: '40' }, + { label: 'Werebeasts', value: '752' }, + { label: 'Exorcism', value: '250' }, + { label: 'Bodyguards', value: '97' }, + { label: 'Hell', value: '336' }, + { label: 'Bickering Couple', value: '84' }, + { label: 'Honest Protagonist', value: '346' }, + { label: 'Fairy Tail', value: '814' }, + { label: 'Divorce', value: '207' }, + { label: 'Spirits', value: '671' }, + { label: 'Unconditional Love', value: '730' }, + { label: 'Reverse Harem', value: '586' }, + { label: 'World Tree', value: '758' }, + { label: 'Criminals', value: '164' }, + { label: 'Skill Books', value: '652' }, + { label: 'Investigations', value: '370' }, + { label: 'Succubus', value: '686' }, + { label: 'Blackmail', value: '88' }, + { label: 'Sentient Objects', value: '620' }, + { label: 'Goblins', value: '311' }, + { label: 'Different Social Status', value: '199' }, + { label: 'Hospital', value: '347' }, + { label: 'Genshin Impact', value: '815' }, + { label: 'Stubborn Protagonist', value: '683' }, + { label: 'Sickly Characters', value: '646' }, + { label: 'Servants', value: '623' }, + { label: 'Disabilities', value: '200' }, + { label: 'Lord', value: '823' }, + { label: 'Returning from Another World', value: '584' }, + { label: 'Cute Story', value: '176' }, + { label: 'Unlucky Protagonist', value: '736' }, + { label: 'Life Script', value: '824' }, + { label: 'Netorare', value: '480' }, + { label: 'Heaven', value: '334' }, + { label: 'Spear Wielder', value: '666' }, + { label: 'Inscriptions', value: '365' }, + { label: 'Engineer', value: '239' }, + { label: 'Lord of the Mysteries', value: '799' }, + { label: 'Masochistic Characters', value: '427' }, + { label: 'Possession', value: '542' }, + { label: 'Conditional Power', value: '149' }, + { label: 'Familiars', value: '256' }, + { label: 'Healers', value: '332' }, + { label: 'Slave Harem', value: '654' }, + { label: 'Herbalist', value: '338' }, + { label: 'Kind Love Interests', value: '378' }, + { label: 'Devouring', value: '797' }, + { label: 'League of Legends', value: '791' }, + { label: 'Mpreg', value: '454' }, + { label: 'Famous Parents', value: '260' }, + { label: 'Love at First Sight', value: '402' }, + { label: 'Heavenly Defying Comprehension', value: '803' }, + { label: 'Basketball', value: '809' }, + { label: 'Hated Protagonist', value: '331' }, + { label: 'Fallen Angels', value: '253' }, + { label: 'Dragon Slayers', value: '215' }, + { label: 'Seme Protagonist', value: '618' }, + { label: 'Legends', value: '389' }, + { label: 'Fleet Battles', value: '282' }, + { label: 'Blood Manipulation', value: '92' }, + { label: 'Court Official', value: '159' }, + { label: 'Summoned Hero', value: '690' }, + { label: 'Androids', value: '37' }, + { label: 'Lottery', value: '401' }, + { label: 'Game of Thrones', value: '813' }, + { label: 'Fat to Fit', value: '270' }, + { label: 'Priests', value: '553' }, + { label: "Seeing Things Other Humans Can't", value: '615' }, + { label: 'Shoujo-Ai Subplot', value: '638' }, + { label: 'Twisted Personality', value: '727' }, + { label: 'Magical Girls', value: '413' }, + { label: 'Sadistic Characters', value: '596' }, + { label: 'Enlightenment', value: '240' }, + { label: 'Prostitutes', value: '558' }, + { label: 'Weak Protagonist', value: '749' }, + { label: 'Copy', value: '807' }, + { label: 'Adapted to Game', value: '13' }, + { label: 'Puppeteers', value: '564' }, + { label: 'Sealed Power', value: '605' }, + { label: 'Cohabitation', value: '140' }, + { label: 'Mob Protagonist', value: '444' }, + { label: 'Seven Deadly Sins', value: '624' }, + { label: 'Single Parent', value: '649' }, + { label: 'Drugs', value: '218' }, + { label: 'Territory Management', value: '802' }, + { label: 'Druids', value: '219' }, + { label: 'Kidnappings', value: '377' }, + { label: 'R-15', value: '567' }, + { label: 'Brainwashing', value: '101' }, + { label: 'Overprotective Siblings', value: '507' }, + { label: 'Gambling', value: '296' }, + { label: 'Arms Dealers', value: '52' }, + { label: 'Manly Gay Couple', value: '423' }, + { label: 'Unique Weapons', value: '734' }, + { label: 'Lawyers', value: '386' }, + { label: 'Anal', value: '33' }, + { label: 'Time Loop', value: '706' }, + { label: 'Grinding', value: '320' }, + { label: 'Slave Protagonist', value: '655' }, + { label: 'Hypnotism', value: '354' }, + { label: 'Demon Slayer', value: '812' }, + { label: 'Unreliable Narrator', value: '737' }, + { label: 'Unique Weapon User', value: '733' }, + { label: 'Poetry', value: '532' }, + { label: 'Philosophical', value: '522' }, + { label: 'Feng Shui', value: '277' }, + { label: 'Chuunibyou', value: '131' }, + { label: 'Reality-Game Fusion', value: '830' }, + { label: 'Dragon Riders', value: '214' }, + { label: 'Omegaverse', value: '493' }, + { label: 'Suicides', value: '689' }, + { label: 'Love Rivals', value: '404' }, + { label: 'Stoic Characters', value: '674' }, + { label: 'Monster Girls', value: '449' }, + { label: 'Trickster', value: '724' }, + { label: 'Handjob', value: '326' }, + { label: 'Limited Lifespan', value: '392' }, + { label: 'Restaurant', value: '582' }, + { label: 'Fallen Nobility', value: '254' }, + { label: 'Masturbation', value: '430' }, + { label: 'Dishonest Protagonist', value: '203' }, + { label: 'Dungeon Master', value: '220' }, + { label: 'Serial Killers', value: '622' }, + { label: 'Younger Brothers', value: '762' }, + { label: 'Pharmacist', value: '521' }, + { label: 'Secret Relationship', value: '610' }, + { label: 'Living Alone', value: '394' }, + { label: 'Mangaka', value: '421' }, + { label: 'Childish Protagonist', value: '130' }, + { label: 'Office Romance', value: '491' }, + { label: 'Models', value: '445' }, + { label: 'Human Weapon', value: '350' }, + { label: 'Fanaticism', value: '262' }, + { label: 'Pilots', value: '528' }, + { label: 'Lovers Reunited', value: '406' }, + { label: 'Blind Protagonist', value: '91' }, + { label: 'Rebellion', value: '573' }, + { label: 'Programmer', value: '556' }, + { label: 'Flashbacks', value: '281' }, + { label: 'Forced into a Relationship', value: '284' }, + { label: 'More Children More Blessings', value: '825' }, + { label: "Sibling's Care", value: '643' }, + { label: 'Helpful Protagonist', value: '337' }, + { label: 'Fat Protagonist', value: '269' }, + { label: 'Awkward Protagonist', value: '67' }, + { label: 'Spiritual Energy Revival', value: '828' }, + { label: 'Distrustful Protagonist', value: '204' }, + { label: 'Folklore', value: '283' }, + { label: 'Engagement', value: '238' }, + { label: 'Half-human Protagonist', value: '325' }, + { label: 'Wishes', value: '753' }, + { label: 'Tomboyish Female Lead', value: '712' }, + { label: 'Shapeshifters', value: '631' }, + { label: 'Love Triangles', value: '405' }, + { label: 'Shounen-Ai Subplot', value: '639' }, + { label: 'Shy Characters', value: '641' }, + { label: 'Reborn as the Villain', value: '831' }, + { label: 'Body Swap', value: '94' }, + { label: 'Coming of Age', value: '147' }, + { label: 'Online Romance', value: '495' }, + { label: 'DnD', value: '794' }, + { label: 'Kuudere', value: '382' }, + { label: 'Monster Society', value: '450' }, + { label: 'Adapted to Drama CD', value: '12' }, + { label: 'Spirit Users', value: '670' }, + { label: 'Trap', value: '722' }, + { label: 'Orgy', value: '499' }, + { label: 'Inferiority Complex', value: '363' }, + { label: 'Unrequited Love', value: '738' }, + { label: 'Genderless Protagonist', value: '302' }, + { label: 'Elderly Protagonist', value: '231' }, + { label: 'Tentacles', value: '700' }, + { label: 'Clumsy Love Interests', value: '138' }, + { label: 'Library', value: '391' }, + { label: 'Parasites', value: '511' }, + { label: 'Sentimental Protagonist', value: '621' }, + { label: 'Mysterious Illness', value: '469' }, + { label: 'Spies', value: '668' }, + { label: 'Dead Protagonist', value: '182' }, + { label: 'Former Hero', value: '288' }, + { label: 'Cousins', value: '160' }, + { label: 'Seduction', value: '614' }, + { label: 'Interconnected Storylines', value: '367' }, + { label: 'Jujutsu Kaisen', value: '776' }, + { label: 'Curious Protagonist', value: '172' }, + { label: 'Stockholm Syndrome', value: '673' }, + { label: 'Genies', value: '305' }, + { label: 'Time Paradox', value: '708' }, + { label: 'Mind Break', value: '438' }, + { label: 'Polite Protagonist', value: '535' }, + { label: 'Bookworm', value: '99' }, + { label: 'Transported Modern Structure', value: '720' }, + { label: 'Bestiality', value: '82' }, + { label: 'Childhood Promise', value: '129' }, + { label: 'Parent Complex', value: '512' }, + { label: 'Sibling Rivalry', value: '642' }, + { label: 'BDSM', value: '77' }, + { label: 'Eunuch', value: '242' }, + { label: 'Introverted Protagonist', value: '369' }, + { label: 'Affair', value: '23' }, + { label: 'Autism', value: '63' }, + { label: 'Matriarchy', value: '431' }, + { label: 'Selfless Protagonist', value: '617' }, + { label: 'Automatons', value: '64' }, + { label: 'Business Wars', value: '806' }, + { label: 'Quiet Characters', value: '565' }, + { label: 'Depression', value: '194' }, + { label: 'Siblings', value: '644' }, + { label: 'Polyandry', value: '537' }, + { label: 'Western Names', value: '788' }, + { label: 'Terrorists', value: '702' }, + { label: 'Ugly Protagonist', value: '728' }, + { label: 'Rich to Poor', value: '589' }, + { label: 'Reincarnated as an Object', value: '575' }, + { label: 'Antique Shop', value: '44' }, + { label: 'Amusement Park', value: '32' }, + { label: 'Nurses', value: '489' }, + { label: 'Friends Become Enemies', value: '290' }, + { label: 'Sculptors', value: '604' }, + { label: 'Forgetful Protagonist', value: '287' }, + { label: 'Siheyuan', value: '820' }, + { label: 'Invisibility', value: '371' }, + { label: 'Schizophrenia', value: '602' }, + { label: 'Voice Actors', value: '744' }, + { label: 'Apartment Life', value: '45' }, + { label: 'Terminal Illness', value: '701' }, + { label: 'Adapted to Manhwa', value: '16' }, + { label: 'Nightmares', value: '483' }, + { label: 'Adapted to Movie', value: '17' }, + { label: 'Priestesses', value: '552' }, + { label: 'Co-Workers', value: '139' }, + { label: 'Undead Protagonist', value: '810' }, + { label: 'Disfigurement', value: '202' }, + { label: 'Golems', value: '317' }, + { label: 'Dystopia', value: '223' }, + { label: 'Sharing A Body', value: '632' }, + { label: 'Witcher', value: '819' }, + { label: 'Based on a Visual Novel', value: '73' }, + { label: 'Reporters', value: '581' }, + { label: 'Onmyouji', value: '496' }, + { label: 'Identity Crisis', value: '355' }, + { label: 'Language Barrier', value: '384' }, + { label: 'Part-Time Job', value: '514' }, + { label: 'Clubs', value: '137' }, + { label: 'Long-distance Relationship', value: '399' }, + { label: 'Forced Living Arrangements', value: '285' }, + { label: 'Paizuri', value: '509' }, + { label: 'Cunnilingus', value: '170' }, + { label: 'War Records', value: '747' }, + { label: 'Rivalry', value: '591' }, + { label: 'Loneliness', value: '396' }, + { label: 'Pretend Lovers', value: '550' }, + { label: 'Photography', value: '525' }, + { label: 'Timid Protagonist', value: '711' }, + { label: 'Youkai', value: '761' }, + { label: 'Astrologers', value: '62' }, + { label: 'Cosplay', value: '157' }, + { label: 'Adapted from Manga', value: '8' }, + { label: 'Confinement', value: '151' }, + { label: 'Reversible Couple', value: '588' }, + { label: 'Blind Dates', value: '90' }, + { label: 'Eavesdropping', value: '798' }, + { label: 'Neet', value: '479' }, + { label: 'Star Wars', value: '817' }, + { label: 'Stalkers', value: '672' }, + { label: 'Outcasts', value: '503' }, + { label: 'Secret Crush', value: '607' }, + { label: 'Female to Male', value: '276' }, + { label: 'Anti-Magic', value: '41' }, + { label: 'Valkyries', value: '739' }, + { label: 'Sex Friends', value: '626' }, + { label: 'Non-linear Storytelling', value: '487' }, + { label: 'Straight Uke', value: '677' }, + { label: 'Galge', value: '295' }, + { label: 'Mute Character', value: '467' }, + { label: 'Jobless Class', value: '375' }, + { label: 'Glasses-wearing Love Interests', value: '309' }, + { label: 'Shikigami', value: '635' }, + { label: 'Faith Dependent Deities', value: '808' }, + { label: 'Delusions', value: '187' }, + { label: 'Delinquents', value: '186' }, + { label: 'Dancers', value: '177' }, + { label: 'Award-winning Work', value: '66' }, + { label: 'Conflicting Loyalties', value: '152' }, + { label: 'Coma', value: '145' }, + { label: 'Futanari', value: '293' }, + { label: 'Divine Protection', value: '206' }, + { label: 'Guardian Relationship', value: '321' }, + { label: 'Grave Keepers', value: '319' }, + { label: 'Mismatched Couple', value: '441' }, + { label: 'Outdoor Intercourse', value: '504' }, + { label: 'Incubus', value: '360' }, + { label: 'Seven Virtues', value: '625' }, + { label: 'Sign Language', value: '647' }, + { label: 'Debts', value: '185' }, + { label: 'Nudity', value: '488' }, + { label: 'Roommates', value: '593' }, + { label: 'Shota', value: '637' }, + { label: 'Heterochromia', value: '340' }, + { label: 'Indecisive Protagonist', value: '361' }, + { label: 'Precognition', value: '548' }, + { label: 'Frieren', value: '816' }, + { label: 'Adapted to Visual Novel', value: '18' }, + { label: 'Collection of Short Stories', value: '143' }, + { label: 'Cryostasis', value: '168' }, + { label: 'Bands', value: '68' }, + { label: 'Netorase', value: '481' }, + { label: 'Otome Game', value: '502' }, + { label: 'Bisexual Protagonist', value: '86' }, + { label: 'Homunculus', value: '345' }, + { label: 'Voyeurism', value: '745' }, + { label: 'Gladiators', value: '308' }, + { label: 'Student Council', value: '684' }, + { label: 'Samurai', value: '599' }, + { label: 'Social Outcasts', value: '661' }, + { label: 'Misandry', value: '440' }, + { label: 'Fujoshi', value: '292' }, + { label: 'Glasses-wearing Protagonist', value: '310' }, + { label: 'Butlers', value: '110' }, + { label: 'Adapted from Manhua', value: '9' }, + { label: 'Sleeping', value: '657' }, + { label: 'Overlord', value: '826' }, + { label: 'Oneshot', value: '494' }, + { label: 'Imaginary Friend', value: '356' }, + { label: 'Jiangshi', value: '374' }, + { label: 'Array', value: '822' }, + { label: 'Based on a Song', value: '70' }, + { label: 'Hong Kong', value: '821' }, + { label: 'Waiters', value: '746' }, + { label: 'JSDF', value: '376' }, + { label: 'Short Story', value: '636' }, + { label: 'Vocaloid', value: '743' }, + { label: 'Living Abroad', value: '393' }, + { label: 'Shield User', value: '634' }, + { label: 'Editors', value: '229' }, + { label: 'Reluctant Protagonist', value: '580' }, + { label: 'Kimetsu no Yaiba', value: '805' }, + { label: 'Toys', value: '714' }, + { label: 'Classic', value: '133' }, + { label: 'Breast Fetish', value: '102' }, + { label: 'Exhibitionism', value: '249' }, + { label: 'Pacifist Protagonist', value: '508' }, + { label: 'Body-double', value: '96' }, + { label: 'Reborn', value: '829' }, + { label: 'Straight Seme', value: '676' }, + { label: 'Phobias', value: '523' }, + { label: 'Salaryman', value: '598' }, + { label: 'Hikikomori', value: '344' }, + { label: 'All-Girls School', value: '29' }, + { label: 'Senpai-Kouhai Relationship', value: '619' }, + ], + }, + tag_operator: { + value: 'and', + label: 'Tag (And/Or)', + options: [ + { label: 'And', value: 'and' }, + { label: 'Or', value: 'or' }, + ], + type: FilterTypes.Picker, + }, + + tags: { + label: 'Tags', + type: FilterTypes.ExcludableCheckboxGroup, + value: { + include: [], + exclude: [], + }, + options: [ + { label: 'Male Protagonist', value: '417' }, + { label: 'Transmigration', value: '717' }, + { label: 'System', value: '696' }, + { label: 'Cultivation', value: '169' }, + { label: 'Special Abilities', value: '667' }, + { label: 'Female Protagonist', value: '275' }, + { label: 'Fanfiction', value: '263' }, + { label: 'Weak to Strong', value: '750' }, + { label: 'Handsome Male Lead', value: '327' }, + { label: 'Beautiful Female Lead', value: '81' }, + { label: 'Game Elements', value: '297' }, + { label: 'Cheats', value: '122' }, + { label: 'Genius Protagonist', value: '306' }, + { label: 'Reincarnation', value: '578' }, + { label: 'Harem-seeking Protagonist', value: '329' }, + { label: 'Time Travel', value: '710' }, + { label: 'Overpowered Protagonist', value: '506' }, + { label: 'Modern Day', value: '446' }, + { label: 'Business Management', value: '108' }, + { label: 'Calm Protagonist', value: '111' }, + { label: 'Magic', value: '410' }, + { label: 'Immortals', value: '357' }, + { label: 'Clever Protagonist', value: '134' }, + { label: 'Ruthless Protagonist', value: '595' }, + { label: 'Apocalypse', value: '47' }, + { label: 'World Hopping', value: '756' }, + { label: 'Poor to Rich', value: '540' }, + { label: 'Douluo Dalu', value: '772' }, + { label: 'Naruto', value: '769' }, + { label: 'Farming', value: '266' }, + { label: 'Fantasy World', value: '265' }, + { label: 'Kingdom Building', value: '379' }, + { label: 'Fast Cultivation', value: '267' }, + { label: 'Protagonist Strong from the Start', value: '560' }, + { label: 'Cunning Protagonist', value: '171' }, + { label: 'Nationalism', value: '476' }, + { label: 'Schemes And Conspiracies', value: '601' }, + { label: 'Survival', value: '692' }, + { label: 'Post-apocalyptic', value: '544' }, + { label: 'Hard-Working Protagonist', value: '328' }, + { label: 'Showbiz', value: '640' }, + { label: 'Unlimited Flow', value: '735' }, + { label: 'Demons', value: '191' }, + ], + }, + + folders: { + value: '', + label: 'Library Folders', + options: [ + { label: 'No Filter', value: '' }, + { label: 'Reading', value: '1' }, + { label: 'Read Later', value: '2' }, + { label: 'Completed', value: '3' }, + { label: 'Trash', value: '5' }, + ], + type: FilterTypes.Picker, + }, + library_exclude: { + value: '', + label: 'Library Exclude', + options: [ + { label: 'None', value: '' }, + { label: 'Exclude All', value: 'history' }, + { label: 'Exclude Trash', value: 'trash' }, + { label: 'Exclude Library & Trash', value: 'in_library' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +type NovelJson = { + props: Props; + page: string; + query?: { raw_id: number }; +}; + +type Props = { + pageProps: PageProps; + __N_SSP: boolean; +}; + +type PageProps = { + serie: Serie; + server_time: Date; +}; + +type Serie = { + serie_data: SerieData; + chapter: Chapter; + recommendation: SerieData[]; + chapter_data: ChapterData; + id: number; + raw_id: number; + slug: string; + data: Data; + is_default: boolean; + raw_type: string; +}; + +type Chapter = { + serie_id: number; + id: number; + raw_id: number; + order: number; + slug: string; + title: string; + name: string; + created_at: string; + updated_at: string; +}; +type ApiChapter = { + serie_id: number; + id: number; + order: number; + title: string; + name: string; + updated_at: string; +}; + +// type GlossaryTerm = { +// index: number; +// english: string; +// chinese: string; +// symbol: string; +// }; +type ChapterData = { + data: ChapterContent; +}; +type ChapterContent = { + title: string; + body: string; + glossary_data?: { + terms: string[][]; + }; +}; + +type SerieData = { + serie_id?: number; + recommendation_id?: number; + score?: string; + id: number; + slug: string; + search_text: string; + status: number; + data: Data; + created_at: string; + updated_at: string; + view: number; + in_library: number; + rating: number | null; + chapter_count: number; + power: number; + total_rate: number; + user_status: number; + verified: boolean; + from: null; + raw_id: number; + genres?: number[]; +}; + +type Data = { + title: string; + author: string; + description: string; + image: string; +}; + +type JsonNovel = { + success: boolean; + data: Datum[]; +}; +type Datum = { + serie: Serie; + chapters: Chapter[]; + updated_at: Date; + raw_id: number; + slug: string; + data: Data; +}; + +export default new WTRLAB(); diff --git a/plugins/english/wuxiaworld.ts b/plugins/english/wuxiaworld.ts new file mode 100644 index 000000000..0335b60b0 --- /dev/null +++ b/plugins/english/wuxiaworld.ts @@ -0,0 +1,468 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi, fetchProto } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; + +type StringValue = { + value: string; +}; + +type BoolValue = { + value: boolean; +}; + +type DecimalValue = { + units: bigint; + nanos: number; +}; + +type Timestamp = { + seconds: number; + nanos: number; +}; + +type NovelEntry = { + name: string; + coverUrl: string; + slug: string; +}; + +type RelatedChapterUserInfo = { + isChapterUnlocked?: BoolValue; +}; + +type ChapterItem = { + entityId: number; + name: string; + slug: string; + number?: DecimalValue; + content?: StringValue; + relatedUserInfo?: RelatedChapterUserInfo; + offset: number; + publishedAt?: Timestamp; +}; + +type GetChapterResponse = { + item?: ChapterItem; +}; + +type ChapterGroupCounts = { + total: number; + advance: number; + normal: number; +}; + +type ChapterGroupItem = { + id: number; + title: string; + order: number; + fromChapterNumber?: DecimalValue; + toChapterNumber?: DecimalValue; + chapterList: ChapterItem[]; + counts?: ChapterGroupCounts; +}; + +type GetChapterListResponse = { + items: ChapterGroupItem[]; +}; + +enum NovelItem_Status { + Finished = 0, + Active = 1, + Hiatus = 2, + All = -1, +} + +type NovelKarmaInfo = { + isActive: boolean; + isFree: boolean; + maxFreeChapter?: DecimalValue; + canUnlockWithVip: boolean; +}; + +type NovelItem = { + id: number; + name: string; + slug: string; + status: NovelItem_Status; + visible: boolean; + description?: StringValue; + synopsis?: StringValue; + coverUrl?: StringValue; + translatorName?: StringValue; + authorName?: StringValue; + karmaInfo?: NovelKarmaInfo; + genres: string[]; +}; + +type GetNovelResponse = { + item?: NovelItem; +}; + +class WuxiaWorld implements Plugin.PluginBase { + id = 'wuxiaworld'; + name = 'Wuxia World'; + icon = 'src/en/wuxiaworld/icon.png'; + site = 'https://www.wuxiaworld.com/'; + apiSite = 'https://api2.wuxiaworld.com/wuxiaworld.api.v2.'; + filters?: Filters | undefined; + version = '0.5.1'; + + parseNovels(data: { items: NovelEntry[] }) { + const novels: Plugin.NovelItem[] = []; + + data.items.map((novel: NovelEntry) => { + const name = novel.name; + const cover = novel.coverUrl; + const path = `novel/${novel.slug}/`; + + novels.push({ + name, + cover, + path, + }); + }); + + return novels; + } + + async popularNovels(): Promise<Plugin.NovelItem[]> { + const link = `${this.site}api/novels`; + + const result = await fetchApi(link); + const data = await result.json(); + + return this.parseNovels(data); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const data = await fetchProto<GetNovelResponse>( + { + proto: this.proto, + requestType: 'GetNovelRequest', + responseType: 'GetNovelResponse', + requestData: { slug: novelPath.split('/')[1] }, + }, + this.apiSite + 'Novels/GetNovel', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: data.item?.name || 'Untitled', + cover: data.item?.coverUrl?.value, + summary: parseHTML( + data.item?.description?.value + '\n\n' + data.item?.synopsis?.value, + ).text(), + author: data.item?.authorName?.value, + genres: data.item?.genres.join(','), + chapters: [], + }; + + const status = data.item?.status; + switch (status) { + case NovelItem_Status.Active: + novel.status = NovelStatus.Ongoing; + break; + case NovelItem_Status.Hiatus: + novel.status = NovelStatus.OnHiatus; + break; + case NovelItem_Status.All: + novel.status = NovelStatus.Unknown; + break; + default: + novel.status = NovelStatus.Completed; + } + + // novel.summary = loadedCheerio('.relative > .absolute:first') + // .children('span') + // .map((i,el) => loadedCheerio(el).text().trim()) + // .toArray() + // .join('\n'); + + // novel.genres = loadedCheerio("a.MuiLink-underlineNone") + // .map((i,el) => loadedCheerio(el).text()) + // .toArray() + // .join(','); + + // novel.status = loadedCheerio("div.font-set-b10").text(); + + const list = await fetchProto<GetChapterListResponse>( + { + proto: this.proto, + requestType: 'GetChapterListRequest', + responseType: 'GetChapterListResponse', + requestData: { novelId: data.item?.id }, + }, + this.apiSite + 'Chapters/GetChapterList', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + + const freeChapter = + Number(data.item?.karmaInfo?.maxFreeChapter?.units) + + (data.item?.karmaInfo?.maxFreeChapter?.nanos || 0) / 1000000000 || 50; + + const chapter: Plugin.ChapterItem[] = list.items.flatMap( + (ChapterGroupItem: ChapterGroupItem) => + ChapterGroupItem.chapterList.map((chapterItem: ChapterItem) => ({ + page: ChapterGroupItem.title, + name: + chapterItem.name + + (chapterItem.relatedUserInfo?.isChapterUnlocked?.value === false || + (!chapterItem.relatedUserInfo && + Number(chapterItem.number?.units) + + (chapterItem.number?.nanos || 0) / 1000000000 > + freeChapter) + ? ' 🔒' + : ''), + path: novelPath + chapterItem.slug, + chapterNumber: chapterItem.offset, + releaseTime: new Date( + (chapterItem.publishedAt?.seconds || 0) * 1000 + + (chapterItem.publishedAt?.nanos || 0) / 1000000, + ).toISOString(), + })), + ); + + novel.chapters = chapter; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const paths = chapterPath.split('/'); + const data = await fetchProto<GetChapterResponse>( + { + proto: this.proto, + requestType: 'GetChapterRequest', + responseType: 'GetChapterResponse', + requestData: { + chapterProperty: { + slugs: { + novelSlug: paths[1], + chapterSlug: paths[2], + }, + }, + }, + }, + this.apiSite + 'Chapters/GetChapter', + { + headers: { + 'Content-Type': 'application/grpc-web+proto', + }, + }, + ); + // loadedCheerio(".chapter-nav").remove(); + // loadedCheerio("#chapter-content > script").remove(); + // const chapterText = loadedCheerio("#chapter-content").html() || ''; + + const chapterText = data.item?.content?.value || ''; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = + this.site + 'api/novels/search?query=' + encodeURIComponent(searchTerm); + + const result = await fetchApi(url); + const data = await result.json(); + + return this.parseNovels(data); + } + + proto = ` + syntax = "proto3"; + option optimize_for = CODE_SIZE; + package wuxiaworld.api.v2; + + import public "google/protobuf/wrappers.proto"; + import public "google/protobuf/timestamp.proto"; + + message StringValue { + // The string value. + string value = 1; + } + + message BoolValue { + // The bool value. + bool value = 1; + } + + message Int32Value { + // The int32 value. + int32 value = 1; + } + + message DecimalValue { + // Whole units part of the amount + int64 units = 1; + // Nano units of the amount (10^-9) + // Must be the same sign as units + sfixed32 nanos = 2; + } + + message Timestamp { + int64 seconds = 1; + int32 nanos = 2; + } + + message RelatedChapterUserInfo { + optional BoolValue isChapterUnlocked = 1; + optional BoolValue isNovelUnlocked = 2; + optional BoolValue isChapterFavorite = 3; + optional BoolValue isNovelOwned = 4; + optional BoolValue isChapterOwned = 5; + } + + message ChapterNovelInfo { + int32 id = 1; + string name = 2; + optional StringValue coverUrl = 3; + string slug = 4; + } + + message ChapterParagraph { + string id = 1; + int32 chapterId = 2; + int32 totalComments = 3; + optional StringValue content = 4; + } + + message ChapterItem { + int32 entityId = 1; + string name = 2; + string slug = 3; + optional DecimalValue number = 4; + optional StringValue content = 5; + int32 novelId = 6; + bool visible = 7; + bool isTeaser = 8; + optional Timestamp whenToPublish = 9; + bool spoilerTitle = 10; + bool allowComments = 11; + optional ChapterNovelInfo novelInfo = 14; + optional RelatedChapterUserInfo relatedUserInfo = 16; + int32 offset = 17; + optional Timestamp publishedAt = 18; + optional StringValue translatorThoughts = 19; + repeated ChapterParagraph paragraphs = 21; + } + + message ChapterGroupCounts { + int32 total = 1; + int32 advance = 2; + int32 normal = 3; + } + + message ChapterGroupItem { + int32 id = 1; + string title = 2; + int32 order = 3; + optional DecimalValue fromChapterNumber = 4; + optional DecimalValue toChapterNumber = 5; + repeated ChapterItem chapterList = 6; + optional ChapterGroupCounts counts = 7; + } + + message GetChapterListRequest { + int32 novelId = 1; + message BaseChapterInfo { + oneof chapterInfo { + int32 chapterId = 1; + string slug = 2; + int32 offset = 3; + } + } + message FilterChapters { + optional Int32Value chapterGroupId = 1; + optional BoolValue isAdvanceChapter = 2; + optional BaseChapterInfo baseChapter = 3; + } + optional FilterChapters filter = 2; + optional Int32Value count = 3; + } + + message GetChapterListResponse { + repeated ChapterGroupItem items = 1; + optional ChapterNovelInfo novelInfo = 2; + } + + message GetChapterByProperty { + message ByNovelAndChapterSlug { + string novelSlug = 1; + string chapterSlug = 2; + } + oneof byProperty { + int32 chapterId = 1; + ByNovelAndChapterSlug slugs = 2; + } + } + + message GetChapterRequest { + optional GetChapterByProperty chapterProperty = 1; + } + + message GetChapterResponse { + optional ChapterItem item = 1; + } + + message NovelKarmaInfo { + bool isActive = 1; + bool isFree = 2; + optional DecimalValue maxFreeChapter = 3; + bool canUnlockWithVip = 4; + } + + message NovelItem { + int32 id = 1; + string name = 2; + string slug = 3; + enum Status { + Finished = 0; + Active = 1; + Hiatus = 2; + All = -1; + } + Status status = 4; + bool visible = 7; + optional StringValue description = 8; + optional StringValue synopsis = 9; + optional StringValue coverUrl = 10; + optional StringValue translatorName = 11; + optional StringValue authorName = 13; + optional NovelKarmaInfo karmaInfo = 14; + repeated string genres = 16; + } + + message GetNovelRequest { + oneof selector { + int32 id = 1; + string slug = 2; + } + } + + message GetNovelResponse { + optional NovelItem item = 1; + } + + service Chapters { + rpc GetChapterList(GetChapterListRequest) returns (GetChapterListResponse); + rpc GetChapter(GetChapterRequest) returns (GetChapterResponse); + } + + service Novels { + rpc GetNovel(GetNovelRequest) returns (GetNovelResponse); + } + `; +} + +export default new WuxiaWorld(); diff --git a/plugins/french/chireads.ts b/plugins/french/chireads.ts new file mode 100644 index 000000000..1d08feaa5 --- /dev/null +++ b/plugins/french/chireads.ts @@ -0,0 +1,339 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class ChireadsPlugin implements Plugin.PluginBase { + id = 'chireads'; + name = 'Chireads'; + icon = 'src/fr/chireads/icon.png'; + site = 'https://chireads.com'; + version = '1.0.2'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels( + pageNo: number, + { filters, showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + let url = this.site; + let tag = 'all'; + if (showLatestNovels) url += '/category/translatedtales/page/' + pageNo; + else { + if ( + filters && + typeof filters.tag.value === 'string' && + filters.tag.value !== 'all' + ) + tag = filters.tag.value; + if (tag !== 'all') url += '/tag/' + tag + '/page/' + pageNo; + else if (pageNo > 1) return []; + } + let $ = await this.getCheerio(url); + + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + + if (showLatestNovels || tag !== 'all') { + let loop = 1; + if (showLatestNovels) loop = 2; + for (let i = 0; i < loop; i++) { + if (i === 1) + $ = await this.getCheerio( + this.site + '/category/original/page/' + pageNo, + ); + let romans = $('.romans-content li'); + if (!romans.length) romans = $('#content li'); + romans.each((i, elem) => { + const novelName = $(elem) + .contents() + .find('div') + .first() + .text() + .trim(); + const novelCover = $(elem) + .find('div') + .first() + .find('img') + .attr('src'); + const novelUrl = $(elem).find('div').first().find('a').attr('href'); + + if (novelUrl) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + } + } else { + const populaire = $(':contains("Populaire")') + .last() + .parent() + .next() + .find('li > div'); + if (populaire.length === 12) { + // pc + let novelCover: string | undefined; + let novelName: string | undefined; + let novelUrl: string | undefined; + populaire.each((i, elem) => { + if (i % 2 === 0) novelCover = $(elem).find('img').attr('src'); + else { + novelName = $(elem).text().trim(); + novelUrl = $(elem).find('a').attr('href'); + + if (!novelUrl) return; + + novel = { + name: novelName, + cover: novelCover || defaultCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + } // mobile + else { + const imgs = populaire.find('div.popular-list-img img'); + const txts = populaire.find('div.popular-list-name'); + + txts.each((i, elem) => { + const novelName = $(elem).text().trim(); + const novelCover = $(imgs[i]).attr('src'); + const novelUrl = $(elem).find('a').attr('href'); + + if (novelUrl) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + } + } + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { path: novelPath, name: 'Sans titre' }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = + $('.inform-product-txt').first().text().trim() || + $('.inform-title').text().trim(); + novel.cover = + $('.inform-product img').attr('src') || + $('.inform-product-img img').attr('src') || + defaultCover; + novel.summary = + $('.inform-inform-txt').text().trim() || + $('.inform-intr-txt').text().trim(); + + const infos = + $('div.inform-product-txt > div.inform-intr-col').text().trim() || + $('div.inform-inform-data > h6').text().trim(); + if (infos.includes('Auteur : ')) + novel.author = infos + .substring( + infos.indexOf('Auteur : ') + 9, + infos.indexOf('Statut de Parution : '), + ) + .trim(); + else if (infos.includes('Fantrad : ')) + novel.author = infos + .substring( + infos.indexOf('Fantrad : ') + 10, + infos.indexOf('Statut de Parution : '), + ) + .trim(); + else novel.author = 'Inconnu'; + switch ( + infos.substring(infos.indexOf('Statut de Parution : ') + 21).toLowerCase() + ) { + case 'en pause': + novel.status = NovelStatus.OnHiatus; + break; + case 'complet': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Ongoing; + break; + } + + const chapters: Plugin.ChapterItem[] = []; + + let chapterList = $('.chapitre-table a'); + if (!chapterList.length) { + $('div.inform-annexe-list').first().remove(); + chapterList = $('.inform-annexe-list').find('a'); + } + chapterList.each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href'); + const releaseDate = dayjs( + chapterUrl?.substring(chapterUrl.length - 11, chapterUrl.length - 1), + ).format('DD MMMM YYYY'); + + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + + novel.chapters = chapters; + + return novel; + } + + async parseChapter(chapterUrl: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterUrl); + + const chapterText = $('#content').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + let novels: Plugin.NovelItem[] = []; + + let i = 1; + let finised = false; + while (!finised) { + await this.popularNovels(i, { + showLatestNovels: true, + filters: undefined, + }).then(res => { + if (res.length === 0) finised = true; + novels.push(...res); + }); + i++; + } + + novels = novels.filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''), + ), + ); + + return novels; + } + + filters = { + tag: { + type: FilterTypes.Picker, + label: 'Tag', + value: 'all', + options: [ + { label: 'Tous', value: 'all' }, + { label: 'Arts martiaux', value: 'arts-martiaux' }, + { label: 'De faible à fort', value: 'de-faible-a-fort' }, + { label: 'Adapté en manhua', value: 'adapte-en-manhua' }, + { label: 'Cultivation', value: 'cultivation' }, + { label: 'Action', value: 'action' }, + { label: 'Aventure', value: 'aventure' }, + { label: 'Monstres', value: 'monstres' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Fantastique', value: 'fantastique' }, + { label: 'Adapté en Animé', value: 'adapte-en-anime' }, + { label: 'Alchimie', value: 'alchimie' }, + { label: 'Éléments de jeux', value: 'elements-de-jeux' }, + { label: 'Calme Protagoniste', value: 'calme-protagoniste' }, + { + label: 'Protagoniste intelligent', + value: 'protagoniste-intelligent', + }, + { label: 'Polygamie', value: 'polygamie' }, + { label: 'Belle femelle Lea', value: 'belle-femelle-lea' }, + { label: 'Personnages arrogants', value: 'personnages-arrogants' }, + { label: 'Système de niveau', value: 'systeme-de-niveau' }, + { label: 'Cheat', value: 'cheat' }, + { label: 'Protagoniste génie', value: 'protagoniste-genie' }, + { label: 'Comédie', value: 'comedie' }, + { label: 'Gamer', value: 'gamer' }, + { label: 'Mariage', value: 'mariage' }, + { label: 'seeking Protag', value: 'seeking-protag' }, + { label: 'Romance précoce', value: 'romance-precoce' }, + { label: 'Croissance accélérée', value: 'croissance-acceleree' }, + { label: 'Artefacts', value: 'artefacts' }, + { + label: 'Intelligence artificielle', + value: 'intelligence-artificielle', + }, + { label: 'Mariage arrangé', value: 'mariage-arrange' }, + { label: 'Mature', value: 'mature' }, + { label: 'Adulte', value: 'adulte' }, + { + label: 'Administrateur de système', + value: 'administrateur-de-systeme', + }, + { label: 'Beau protagoniste', value: 'beau-protagoniste' }, + { + label: 'Protagoniste charismatique', + value: 'protagoniste-charismatique', + }, + { label: 'Protagoniste masculin', value: 'protagoniste-masculin' }, + { label: 'Démons', value: 'demons' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Académie', value: 'academie' }, + { + label: 'Cacher les vraies capacités', + value: 'cacher-les-vraies-capacites', + }, + { + label: 'Protagoniste surpuissant', + value: 'protagoniste-surpuissant', + }, + { label: 'Joueur', value: 'joueur' }, + { + label: 'Protagoniste fort dès le départ', + value: 'protagoniste-fort-des-le-depart', + }, + { label: 'Immortels', value: 'immortels' }, + { label: 'Cultivation rapide', value: 'cultivation-rapide' }, + { label: 'Harem', value: 'harem' }, + { label: 'Assasins', value: 'assasins' }, + { label: 'De pauvre à riche', value: 'de-pauvre-a-riche' }, + { + label: 'Système de classement de jeux', + value: 'systeme-de-classement-de-jeux', + }, + { label: 'Capacités spéciales', value: 'capacites-speciales' }, + { label: 'Vengeance', value: 'vengeance' }, + ], + }, + } satisfies Filters; +} + +export default new ChireadsPlugin(); diff --git a/plugins/french/harkeneliwood.ts b/plugins/french/harkeneliwood.ts new file mode 100644 index 000000000..6631e3afc --- /dev/null +++ b/plugins/french/harkeneliwood.ts @@ -0,0 +1,166 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class HarkenEliwoodPlugin implements Plugin.PluginBase { + id = 'harkeneliwood'; + name = 'HarkenEliwood'; + icon = 'src/fr/harkeneliwood/icon.png'; + site = 'https://harkeneliwood.wordpress.com'; + version = '1.0.0'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + if (pageNo > 1) return []; + + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + const url = this.site; + const $ = await this.getCheerio(url + '/projets/'); + $('#content .entry-content [href]') + // We don't collect items for Facebook and Twitter. + .not('[rel="nofollow noopener noreferrer"]') + .each((i, elem) => { + const novelName = $(elem).text().trim(); + const novelUrl = $(elem).attr('href'); + if (novelUrl && novelName) { + novel = { + name: novelName, + cover: defaultCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + novel.name = $('#content h1.entry-title').text().trim(); + novel.cover = + $('#content .entry-content p img').first().attr('src') || defaultCover; + novel.summary = this.getSummary($('#content .entry-content').text()); + novel.author = this.getAuthor($('#content .entry-content').text()); + novel.status = NovelStatus.Ongoing; + const chapters: Plugin.ChapterItem[] = []; + $('#content .entry-content p a').each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href'); + // Check if the chapter URL exists and contains the site name. + if (chapterUrl && chapterUrl.includes(this.site) && chapterName) { + const releaseDate = dayjs( + chapterUrl?.substring(this.site.length + 1, this.site.length + 11), + ).format('DD MMMM YYYY'); + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: releaseDate, + }); + } + }); + novel.chapters = chapters; + return novel; + } + + getSummary(text: string) { + let resume = ''; + const regexResume1 = /Synopsis :([\s\S]*)Traduction anglaise/i; + const regexResume2 = /Synopsis :([\s\S]*)Raw :/i; + const regexResume3 = /Synopsis 1 :([\s\S]*)Synopsis 2 :([\s\S]*)Raw :/i; + const regexResume4 = /Synopsis :([\s\S]*)Prélude/i; + const regexResume5 = /Synospis :([\s\S]*)Original /i; + const regexResume6 = /([\s\S]*)Raw :/i; + + const match1: RegExpExecArray | null = regexResume1.exec(text); + const match2: RegExpExecArray | null = regexResume2.exec(text); + const match3: RegExpExecArray | null = regexResume3.exec(text); + const match4: RegExpExecArray | null = regexResume4.exec(text); + const match5: RegExpExecArray | null = regexResume5.exec(text); + + if (match1 !== null) { + resume = match1[1]; + } else if (match2 !== null) { + resume = match2[1]; + } else if (match3 !== null) { + resume = match3[1] + match3[2]; + } else if (match4 !== null) { + resume = match4[1]; + } else if (match5 !== null) { + resume = match5[1]; + } else { + resume = text; + } + + if (regexResume6.test(resume)) { + const match6: RegExpExecArray | null = regexResume6.exec(resume); + if (match6 !== null) { + resume = match6[1]; + } + } + + return resume.trim(); + } + + getAuthor(text: string) { + const regexAuteur = /Auteur\s*:\s*(.*?)\s*(?:\r?\n|$)/i; + const match = regexAuteur.exec(text); + + if (match !== null && match[1].trim() !== '') { + return match[1].trim(); + } + + return ''; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + const title = $('h1.entry-title'); + const chapter = $('div.entry-content'); + return (title.html() || '') + (chapter.html() || ''); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } +} + +export default new HarkenEliwoodPlugin(); diff --git a/plugins/french/kisswood.ts b/plugins/french/kisswood.ts new file mode 100644 index 000000000..7d18c2abf --- /dev/null +++ b/plugins/french/kisswood.ts @@ -0,0 +1,240 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class KissWoodPlugin implements Plugin.PluginBase { + id = 'kisswood'; + name = 'KissWood'; + icon = 'src/fr/kisswood/icon.png'; + site = 'https://kisswood.eu'; + version = '1.0.0'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url); + const body = await r.text(); + const $ = load(body); + return $; + } + + async getNovelsCovers( + novels: Plugin.NovelItem[], + listUrlCover: string[], + ): Promise<Plugin.NovelItem[]> { + await Promise.all( + novels.map(async (novel, index) => { + const urlCover = listUrlCover[index]; + if (urlCover) { + novel.cover = this.findCoverImage(await this.getCheerio(urlCover)); + } + }), + ); + return novels; + } + + regexAuthors = [/Auteur :([^\n]*)/, /Auteur\u00A0:([^\n]*)/]; + + async getNovelInfo( + novel: Plugin.SourceNovel, + url: string, + ): Promise<Plugin.SourceNovel> { + const $ = await this.getCheerio(url); + + const textArray: string[] = $('.entry-content p') + .map((_, element) => $(element).text().trim()) + .get() + .join('\n') + .split('\n'); + + const index = textArray.findIndex(element => + [ + 'Traducteur Anglais- Français', + 'Titre en français', + '———', + 'Titre :', + 'Lien vers le premier chapitre', + '____________', + 'Auteur : ', + ].some(marker => element.includes(marker)), + ); + + novel.summary = (index !== -1 ? textArray.slice(0, index) : textArray) + .join('\n') + .replace('Synopsis :', ''); + novel.author = this.extractInfo(textArray.join('\n'), this.regexAuthors); + novel.cover = this.findCoverImage($); + return novel; + } + + findCoverImage($: CheerioAPI): string { + return ( + $('div p img').first().attr('src') || + $('figure a img').first().attr('src') || + $('figure img').first().attr('src') || + defaultCover + ); + } + + extractInfo(text: string, regexes: RegExp[]): string { + for (const regex of regexes) { + const match = regex.exec(text); + if (match !== null) { + return match[1].trim(); + } + } + return ''; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + if (pageNo > 1) return []; + + const novels: Plugin.NovelItem[] = []; + const $ = await this.getCheerio(this.site); + const listUrlCover: string[] = []; + $('nav div div ul li ul li').each((i, elem) => { + if ($(elem).text().trim() === 'Sommaire') { + const novelName = $(elem) + .closest('ul') + .siblings('a') + .first() + .text() + .trim(); + const novelUrl = $(elem).find('a').attr('href'); + + if (novelUrl && novelName) { + const urlCover = $(elem).parent().find('a').attr('href'); + if (urlCover) { + listUrlCover.push(urlCover); + } else { + listUrlCover.push(''); + } + + const novel = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: defaultCover, + }; + novels.push(novel); + } + } + }); + return await this.getNovelsCovers(novels, listUrlCover); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + let novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + status: NovelStatus.Ongoing, + }; + + const $ = await this.getCheerio(this.site + novelPath); + let novelUrl = null; + + $('nav div div ul li ul li').each((i, elem) => { + if ($(elem).find('a').attr('href') === this.site + novelPath) { + novelUrl = $(elem).parent().find('a').first().attr('href'); + novel.name = $(elem).closest('ul').siblings('a').first().text().trim(); + return; + } + }); + + if (novelUrl) { + novel = await this.getNovelInfo(novel, novelUrl); + } + + const chapterSelectors = [ + '.entry-content ul li a', + '.entry-content ul li ul li a', + '.entry-content p a', + '.entry-content li a', + '.entry-content blockquote a', + ].join(', '); + + const chapters: Plugin.ChapterItem[] = []; + $(chapterSelectors).each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href')?.replace('http://', 'https://'); + if ( + chapterUrl && + chapterName && + chapterUrl.includes(this.site) && + // We remove the unnecessary links to Facebook, X, and the homepage from the chapters. + !chapterUrl.includes('share=facebook') && + !chapterUrl.includes('share=x') && + !chapterUrl.includes('/category/traductions/') && + !chapterUrl.includes('/category/tour-des-mondes/') && + // Removal of duplicates + !chapters.some(chapter => this.site + chapter.path === chapterUrl) + ) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + + const elements: string[] = $('.entry-content') + .contents() + .map((_, el) => $.html(el)) + .get(); + + let hrIndexes: number[] = elements + .map((elem, index) => (elem.includes('<hr>') ? index : -1)) + .filter(index => index !== -1); + + if (hrIndexes.length === 0) { + hrIndexes = [ + 0, + elements.findIndex( + element => + element.includes('https://fr.tipeee.com/kisswood/') || + element.includes('>Sommaire</a>') || + element.includes('>Chapitre Suivant</a>') || + element.includes('———————————————————————————-') || + element.includes('share=facebook'), + ), + ]; + } else if (hrIndexes.length === 1) { + hrIndexes.unshift(0); + } else { + hrIndexes[0] += 1; + } + return elements.slice(hrIndexes[0], hrIndexes[1]).join('\n'); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } +} + +export default new KissWoodPlugin(); diff --git a/plugins/french/noveldeglace.ts b/plugins/french/noveldeglace.ts new file mode 100644 index 000000000..10410df1c --- /dev/null +++ b/plugins/french/noveldeglace.ts @@ -0,0 +1,313 @@ +import { load, CheerioAPI } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; + +class NovelDeGlacePlugin implements Plugin.PluginBase { + id = 'noveldeglace'; + name = 'NovelDeGlace'; + icon = 'src/fr/noveldeglace/icon.png'; + site = 'https://noveldeglace.com/'; + version = '1.0.5'; + + async getCheerio(url: string): Promise<CheerioAPI | undefined> { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + if (!r.ok) return undefined; + const body = await r.text(); + const loadedCheerio = load(body); + return loadedCheerio; + } + + parseNovels( + $: CheerioAPI, + showLatestNovels: boolean | undefined, + ): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + + $('article').each((i, el) => { + const cheerio = $(el); + const novelName = cheerio.find('h2').text().trim(); + const novelCover = cheerio.find('img').attr('src'); + let novelUrl: string | undefined; + if (showLatestNovels) + novelUrl = cheerio.find('span.Roman > a').attr('href'); + else novelUrl = cheerio.find('h2 > a').attr('href'); + + if (novelUrl) { + const novel: Plugin.NovelItem = { + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }; + novels.push(novel); + } + }); + + return novels; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site; + if (showLatestNovels) url += 'chapitre'; + else { + let cat_gen = 'all'; + if (filters && typeof filters.categorie_genre.value == 'string') + cat_gen = filters.categorie_genre.value; + if ( + cat_gen != 'all' && + cat_gen != 'categorie_roman' && + cat_gen != 'genre' + ) { + if (cat_gen[0] == 'c') url += 'categorie_roman/' + cat_gen.substring(2); + else if (cat_gen[0] == 'g') url += 'genre/' + cat_gen.substring(2); + } else if (pageNo > 1) + return []; // when asking for all novels, there is only 1 page + else url += 'roman'; + } + url += '/page/' + pageNo; + const $ = await this.getCheerio(url); + if (!$) return []; + return this.parseNovels($, showLatestNovels); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const $ = await this.getCheerio(this.site + novelPath); + if (!$) throw new Error('Failed to load page (open in web view)'); + + const novel: Plugin.SourceNovel = { path: novelPath, name: 'Untitled' }; + + novel.name = $('span.current').text().trim(); + + novel.cover = $('.su-row > div > div > img').attr('src') || defaultCover; + + const novelInfos = $('div[data-title=Tomes] >').toArray(); + novelInfos.pop(); + novelInfos.shift(); + novel.summary = + $('div[data-title=Synopsis]').text().trim() + + '\n\n' + + novelInfos + .map(el => $(el).text()) + .join('\n') + .trim(); + + novel.author = $("strong:contains('Auteur :')") + .parent() + .text() + .replace('Auteur : ', '') + .trim(); + + novel.artist = $("strong:contains('Illustrateur :')") + .parent() + .text() + .replace('Illustrateur :', '') + .trim(); + + const categorie = $('.categorie').text().replace('Catégorie :', '').trim(); + const genres = $('.genre') + .text() + .replace('Genre :', '') + .replace(/, /g, ',') + .trim(); + if (categorie && categorie != 'Autre') novel.genres = categorie; + if (genres) + novel.genres = novel.genres ? novel.genres + ',' + genres : genres; + + const status = $("strong:contains('Statut :')").parent().attr('class'); + switch (status) { + case 'type etat0': + case 'type etat1': + novel.status = NovelStatus.Ongoing; + break; + case 'type etat4': + novel.status = NovelStatus.OnHiatus; + break; + case 'type etat5': + novel.status = NovelStatus.Completed; + break; + case 'type etat6': + novel.status = NovelStatus.Cancelled; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + + const novelChapters: Plugin.ChapterItem[] = []; + + const volumes = $('div[data-title=Tomes] > div').last().contents(); + const hasMultipleVolumes = volumes.length > 1; + + let chapterName = ''; + const site = this.site; + volumes.each((volumeIndex: number, el) => { + if (hasMultipleVolumes) chapterName = 'T.' + (volumeIndex + 1) + ' '; + $(el) + .find('.chpt') + .each((chapterIndex: number, el) => { + const cheerio = $(el); + const newChapterName = + chapterName + cheerio.find('a').first().text().trim() || ''; + if (!cheerio.find('i.fa').length) { + // no parts + const dateHtml = + cheerio.html()?.substring(cheerio.html()?.indexOf('</a>') || 0) || + ''; + const releaseDate = + dateHtml?.substring( + dateHtml.indexOf('(') + 1, + dateHtml.indexOf(')'), + ) || undefined; + const chapterUrl = cheerio.find('a').attr('href'); + if (chapterUrl) { + const chapter: Plugin.ChapterItem = { + name: newChapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(site, ''), + chapterNumber: chapterIndex, + }; + + novelChapters.push(chapter); + } + } // has parts that needs to be added individually + else { + const items = + cheerio.find('i').parent().next().html()?.split('</a>') || []; + items?.shift(); + const dates: string[] = []; + items?.forEach(item => { + // there is a date on every publish parts + dates.push( + item.substring(item.indexOf('(') + 1, item.indexOf(')')), + ); + }); + const hrefs: string[] = []; + cheerio + .find('i') + .parent() + .next() + .find('a') + .each(function () { + hrefs.push(this['attribs']['href']); + }); + if (dates.length == hrefs.length) + dates.forEach((date, index) => { + const chapter: Plugin.ChapterItem = { + name: newChapterName + ' (' + (index + 1) + ')', + releaseTime: date, + path: hrefs[index].replace(site, ''), + chapterNumber: chapterIndex + (index + 1) / 1000, + }; + novelChapters.push(chapter); + }); + } + }); + }); + + novel.chapters = novelChapters; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + if (!$) throw new Error('Failed to load page (open in web view)'); + + $('.mistape_caption').remove(); + const chapterText = + $('.chapter-content').html() || $('.entry-content').html() || ''; + return chapterText; + } + + async searchNovels( + searchTerm: string, + num: number, + ): Promise<Plugin.NovelItem[]> { + if (num !== 1) return []; // only 1 page of results + const url = this.site + 'roman'; + const $ = await this.getCheerio(url); + if (!$) throw new Error('Failed to load page (open in web view)'); + + let novels = this.parseNovels($, false); + + novels = novels.filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } + + filters = { + categorie_genre: { + type: FilterTypes.Picker, + label: 'Catégorie/Genre', + value: 'all', + options: [ + { label: 'Tous', value: 'all' }, + { label: '═══CATÉGORIES═══', value: 'categorie_roman' }, + { label: 'Autre', value: 'c_autre' }, + { label: 'Fille', value: 'c_fille' }, + { label: 'Original', value: 'c_original' }, + { label: 'Roman pour Adulte', value: 'c_roman-pour-adulte' }, + { label: 'Seinen', value: 'c_seinen' }, + { label: 'Shonen', value: 'c_shonen' }, + { label: 'Xuanhuan', value: 'c_xuanhuan' }, + { label: 'Yaoi', value: 'c_yaoi' }, + { label: 'Yuri', value: 'c_yuri' }, + { label: '═══GENRES═══', value: 'genre' }, + { label: 'Action', value: 'g_action' }, + { label: 'Adulte', value: 'g_adulte' }, + { label: 'Anti-Héros', value: 'g_anti-heros' }, + { label: 'Arts Martiaux', value: 'g_arts-martiaux' }, + { label: 'Aventure', value: 'g_aventure' }, + { label: 'Comédie', value: 'g_comedie' }, + { label: 'Drame', value: 'g_drame' }, + { label: 'Ecchi', value: 'g_ecchi' }, + { label: 'Fantastique', value: 'g_fantastique' }, + { label: 'Harem', value: 'g_harem' }, + { label: 'Horreur', value: 'g_horreur' }, + { label: 'Insectes', value: 'g_insectes' }, + { label: 'Lolicon', value: 'g_lolicon' }, + { label: 'Mature', value: 'g_mature' }, + { label: 'Mecha', value: 'g_mecha' }, + { label: 'Mystère', value: 'g_mystere' }, + { label: 'Pas de harem', value: 'g_pas-de-harem' }, + { label: 'Psychologique', value: 'g_psychologique' }, + { label: 'Romance', value: 'g_romance' }, + { label: 'Sci-fi', value: 'g_sci-fi' }, + { label: 'Science-Fiction', value: 'g_science-fiction' }, + { label: 'Shotacon', value: 'g_shotacon' }, + { label: 'Shoujo Ai', value: 'g_shoujo-ai' }, + { label: 'Smut', value: 'g_smut' }, + { label: 'Surnaturel', value: 'g_surnaturel' }, + { label: 'Tragédie', value: 'g_tragedie' }, + { label: 'Tranche de vie', value: 'g_tranche-de-vie' }, + { label: 'Vie scolaire', value: 'g_vie-scolaire' }, + { label: 'Xuanhuan', value: 'g_xuanhuan' }, + ], + }, + } satisfies Filters; +} + +export default new NovelDeGlacePlugin(); diff --git a/plugins/french/novhell.ts b/plugins/french/novhell.ts new file mode 100644 index 000000000..2253c7442 --- /dev/null +++ b/plugins/french/novhell.ts @@ -0,0 +1,206 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class NovhellPlugin implements Plugin.PluginBase { + id = 'novhell'; + name = 'Novhell'; + icon = 'src/fr/novhell/icon.png'; + site = 'https://novhell.org'; + version = '1.0.1'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + if (pageNo > 1) return []; + + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + const url = this.site; + const $ = await this.getCheerio(url); + $('article div div div figure').each((i, elem) => { + let novelName = $(elem) + .find('figcaption span strong') + .first() + .text() + .trim(); + if (!novelName || novelName.trim() === '') { + novelName = $(elem).find('figcaption a strong').first().text().trim(); + } + const novelCover = $(elem).find('a img').attr('src'); + const novelUrl = $(elem).find('a').attr('href'); + + if (novelUrl && novelName) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = + $('meta[property="og:title"]') + .attr('content') + ?.replace('- NovHell', '') || ''; + novel.cover = + $('section div div div div div img').first().attr('src') || defaultCover; + novel.status = NovelStatus.Ongoing; + novel.author = $("strong:contains('Ecrit par ')") + .parent() + .text() + .replace('Ecrit par ', '') + .trim(); + + if (!novel.author) { + novel.author = $("div p:contains('Auteur')") + .text() + .replace('Auteur', '') + .replace(':', '') + .trim(); + } + if (!novel.author) { + novel.author = $("div p:contains('Ecrit par :')") + .text() + .replace('Ecrit par :', '') + .trim(); + } + novel.genres = $("strong:contains('Genre')") + .parent() + .text() + .replace('Genre', '') + .replace(':', '') + .trim(); + if (!novel.genres) { + novel.genres = $("div p:contains('Genre')") + .text() + .replace('Genre', '') + .replace(':', '') + .trim(); + } + novel.summary = $("strong:contains('Synopsis')") + .parent() + .parent() + .text() + .replace('Synopsis', '') + .replace('Synopsis', '') + .replace(':', '') + .trim(); + const chapters: Plugin.ChapterItem[] = []; + + $('main div article div div section div div div div div p a').each( + (i, elem) => { + // Replace non-breaking spaces with a 'normal' space. + const chapterName = $(elem) + .text() + .replace(/\u00A0/g, ' ') + .trim(); + const chapterUrl = $(elem).attr('href'); + // Check if the chapter URL exists and contains the site name. + if (chapterUrl && chapterUrl.includes(this.site)) { + const regex = /Chapitre (\d+)/g; + let chapterNumber = 0; + let match; + while ((match = regex.exec(chapterName)) !== null) { + const number = parseInt(match[1]); + chapterNumber += number; + } + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + chapterNumber: chapterNumber, + }); + } + }, + ); + + // Sort the chapters array based on the chapter numbers. + // We retrieve the chapters in the order 1-6-11-16-21-...... + novel.chapters = chapters.sort((chapterA, chapterB) => { + if ( + chapterA.chapterNumber !== undefined && + chapterB.chapterNumber !== undefined + ) { + return chapterA.chapterNumber - chapterB.chapterNumber; + } + if (chapterA.chapterNumber === undefined) { + return 1; + } else { + return -1; + } + }); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + const sections = $('main article div div section'); + if (sections) { + const numberOfSection = sections.length; + let title; + let positionChapter = 2; + + for (let i = 3; i <= 5; i++) { + title = sections.eq(numberOfSection - i); + if (title.find('h4').length !== 0) { + positionChapter = i - 1; + break; + } + } + const chapter = sections.eq(numberOfSection - positionChapter); + + if (title && chapter) { + return (title.html() || '') + (chapter.html() || ''); + } + } + return ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } +} + +export default new NovhellPlugin(); diff --git a/plugins/french/phenixscans.broken.ts b/plugins/french/phenixscans.broken.ts new file mode 100644 index 000000000..ebb7421b4 --- /dev/null +++ b/plugins/french/phenixscans.broken.ts @@ -0,0 +1,275 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class PhenixScansTradPlugin implements Plugin.PluginBase { + id = 'phenixscans'; + name = 'PhenixScans'; + icon = 'src/fr/phenixscans/icon.png'; + site = 'https://phenixscans.fr'; + version = '1.0.1'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + + let filter = ''; + for (const key in filters) { + if (typeof filters[key].value === 'object') { + for (const value of filters[key].value as string[]) { + filter += `&genre%5B%5D=${value}`; + } + } + } + + const order = showLatestNovels ? 'update' : 'popular'; + const url = `${this.site}/manga/?page=${pageNo}${filter}&status=&type=novel&order=${order}`; + + const $ = await this.getCheerio(url); + $('div div div div a').each((i, elem) => { + const novelName = $(elem).attr('title')?.trim(); + const novelUrl = $(elem).attr('href'); + const novelCover = $(elem).find('div img').attr('src') || defaultCover; + + if (novelUrl && novelName) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = $('h1[itemprop=name]').text().replace('– Novel', '').trim(); + novel.cover = + $('div[itemprop=image] img').first().attr('src') || defaultCover; + novel.author = $('.fmed b:contains(Auteur)+span').text().trim(); + novel.genres = $('.mgen a') + .map(function () { + return $(this).text(); + }) + .get() + .join(', '); + novel.summary = $('.entry-content[itemprop=description]').text().trim(); + novel.status = this.getStatus( + $('.tsinfo .imptdt:contains(Statut)').text().replace('Statut', '').trim(), + ); + + const chapters: Plugin.ChapterItem[] = []; + $('ul li:has(div.chbox):has(div.eph-num)').each((i, elem) => { + const chapterName = $(elem).find('a .chapternum').text().trim(); + const chapterUrl = $(elem).find('a').attr('href'); + const releaseDate = this.parseDate($(elem).find('a .chapterdate').text()); + if (chapterUrl && chapterUrl.includes(this.site) && chapterName) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: releaseDate, + }); + } + }); + novel.chapters = chapters; + return novel; + } + + parseDate(date: string): string { + const monthMapping: Record<string, number> = { + janvier: 1, + fevrier: 2, + mars: 3, + avril: 4, + mai: 5, + juin: 6, + juillet: 7, + aout: 8, + septembre: 9, + octobre: 10, + novembre: 11, + decembre: 12, + }; + + const [day, month, year] = date.split(' '); + return dayjs( + `${day} ${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]} ${year}`, + 'D MMMM YYYY', + ).format('DD MMMM YYYY'); + } + + getStatus(status: string) { + const lowerCaseStatus = status.toLowerCase(); + const ongoing = ['en cours', 'en cours de publication']; + const onhiatus = ['en pause', 'en attente']; + const completed = ['complété', 'fini', 'achevé', 'terminé']; + const cancelled = ['abandonné']; + + if (ongoing.includes(lowerCaseStatus)) { + return NovelStatus.Ongoing; + } else if (onhiatus.includes(lowerCaseStatus)) { + return NovelStatus.OnHiatus; + } else if (completed.includes(lowerCaseStatus)) { + return NovelStatus.Completed; + } else if (cancelled.includes(lowerCaseStatus)) { + return NovelStatus.Cancelled; + } + return NovelStatus.Unknown; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + return $('#readerarea').html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1, { + showLatestNovels: true, + filters: undefined, + }); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } + + filters = { + genre: { + type: FilterTypes.CheckboxGroup, + label: 'Genre', + value: [], + options: [ + { label: 'Action', value: 'action' }, + { + label: 'Action Adventure Fantaisie Psychologique', + value: 'action-adventure-fantaisie-psychologique', + }, + { + label: 'Action Arts martiaux Aventure Fantastique Surnaturel', + value: 'action-arts-martiaux-aventure-fantastique-surnaturel', + }, + { + label: 'Action Aventure Fantastique', + value: 'action-aventure-fantastique', + }, + { label: 'Action Drame Shônen', value: 'action-drame-shonen' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'amitié', value: 'amitie' }, + { label: 'amour', value: 'amour' }, + { label: 'Art-martiaux', value: 'art-martiaux' }, + { label: 'Arts-martiaux', value: 'arts-martiaux' }, + { label: 'Aventure', value: 'aventure' }, + { label: 'Combat', value: 'combat' }, + { label: 'Comedie', value: 'comedie' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Demons', value: 'demons' }, + { label: 'Dragon', value: 'dragon' }, + { label: 'Drama', value: 'drama' }, + { label: 'Drame', value: 'drame' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantaisie', value: 'fantaisie' }, + { label: 'fantastique', value: 'fantastique' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Ghosts', value: 'ghosts' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Historique', value: 'historique' }, + { label: 'Horreur', value: 'horreur' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Josei', value: 'josei' }, + { label: 'longstrip art', value: 'longstrip-art' }, + { label: 'Magic', value: 'magic' }, + { label: 'Magie', value: 'magie' }, + { label: 'Male Protagonist', value: 'male-protagonist' }, + { label: 'manga', value: 'manga' }, + { label: 'Manhwa', value: 'manhwa' }, + { label: 'Manhwa Player', value: 'manhwa-player' }, + { label: 'Manwha', value: 'manwha' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Monstre', value: 'monstre' }, + { label: 'Murim', value: 'murim' }, + { label: 'mystère', value: 'mystere' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Necromancien', value: 'necromancien' }, + { label: 'necromencer', value: 'necromencer' }, + { label: 'Novel', value: 'novel' }, + { label: 'One Shot', value: 'one-shot' }, + { label: 'Over Power MC', value: 'over-power-mc' }, + { label: 'Partenaire', value: 'partenaire' }, + { label: 'Player', value: 'player' }, + { label: 'Player Manhwa', value: 'player-manhwa' }, + { label: 'Portail', value: 'portail' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Psychologique', value: 'psychologique' }, + { label: 'regresseur', value: 'regresseur' }, + { label: 'régression', value: 'regression' }, + { label: 'Réincarnation', value: 'reincarnation' }, + { label: 'Returner', value: 'returner' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shôjo', value: 'shojo' }, + { label: 'Shônen', value: 'shonen' }, + { label: 'Shotacon', value: 'shotacon' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Slide of Life', value: 'slide-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Surnaturel', value: 'surnaturel' }, + { label: 'Système', value: 'systeme' }, + { label: 'Tragédie', value: 'tragedie' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Webtoons', value: 'webtoons' }, + ], + }, + } satisfies Filters; +} + +export default new PhenixScansTradPlugin(); diff --git a/plugins/french/warriorlegendtrad.ts b/plugins/french/warriorlegendtrad.ts new file mode 100644 index 000000000..b411b424d --- /dev/null +++ b/plugins/french/warriorlegendtrad.ts @@ -0,0 +1,187 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class WarriorLegendTradPlugin implements Plugin.PluginBase { + id = 'warriorlegendtrad'; + name = 'Warrior Legend Trad'; + icon = 'src/fr/warriorlegendtrad/icon.png'; + site = 'https://warriorlegendtrad.wordpress.com'; + version = '1.0.1'; + + regexAuthors = [/Auteur\u00A0:([^\n]*)/]; + + regexGenres = [/Genre :([^\n]*)/]; + + regexSummary = [/Synopsis\u00A0:([\s\S]*)index chapitre :/i]; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + if (pageNo > 2) return []; + + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + let url; + // light novel + if (pageNo === 1) { + url = this.site + '/light-novel'; + } + // Original creation + else { + url = this.site + '/crea'; + } + + const $ = await this.getCheerio(url); + $('div div div article').each((i, elem) => { + const novelName = $(elem).find('.entry-wrapper h2').first().text().trim(); + const novelUrl = $(elem).find('.entry-wrapper h2 a').attr('href'); + const novelCover = + $(elem).find('figure a img').attr('src') || defaultCover; + + if (novelUrl && novelName) { + novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = $('.site-main article header h1').text().trim(); + novel.cover = + $('.site-main article figure img').first().attr('src') || defaultCover; + const entryContentText = $('.entry-content').text(); + novel.author = this.extractInfo(entryContentText, this.regexAuthors); + novel.genres = this.extractInfo(entryContentText, this.regexGenres); + novel.summary = this.extractInfo(entryContentText, this.regexSummary); + novel.status = this.getStatus(entryContentText); + + const chapters: Plugin.ChapterItem[] = []; + $('div div article div') + .find('h2 a, h3 a') + .each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href'); + const releaseDate = dayjs( + chapterUrl?.substring(this.site.length + 1, this.site.length + 11), + ).format('DD MMMM YYYY'); + if (chapterUrl && chapterUrl.includes(this.site) && chapterName) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: releaseDate, + }); + } + }); + + //We sort by date because the elements are not in a fixed order, + //and then by name because there are chapters with the same dates. + novel.chapters = chapters.sort((a, b) => { + if (!a.releaseTime) return 1; + if (!b.releaseTime) return -1; + const dateA = new Date(a.releaseTime).getTime(); + const dateB = new Date(b.releaseTime).getTime(); + const dateComparison = dateA - dateB; + if (dateComparison === 0) { + return a.name.localeCompare(b.name); + } + return dateComparison; + }); + return novel; + } + + extractInfo(text: string, regexes: RegExp[]): string { + for (const regex of regexes) { + const match = regex.exec(text); + if (match !== null) { + return match[1].trim(); + } + } + return ''; + } + + getStatus(text: string) { + const regexSummary = [ + /État sur le site :([^\n]*)/i, + /état sur le site:([^\n]*)/i, + ]; + + const status = this.extractInfo(text, regexSummary); + if (status.includes('en cours')) { + return NovelStatus.Ongoing; + } else if (status.includes('en pause')) { + return NovelStatus.OnHiatus; + } else if (status.includes('terminé')) { + return NovelStatus.Completed; + } else if (status.includes('abandonné')) { + return NovelStatus.Cancelled; + } + + return NovelStatus.Ongoing; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + let contenuHtml = ''; + $('.entry-content') + .contents() + .each(function () { + if ( + !$.html(this)?.startsWith('<div') && + !$.html(this)?.startsWith('<hr') && + !$.html(this)?.includes('<script') + ) { + contenuHtml += $.html(this); + } + }); + return contenuHtml; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } +} + +export default new WarriorLegendTradPlugin(); diff --git a/plugins/french/wuxialnscantrad.ts b/plugins/french/wuxialnscantrad.ts new file mode 100644 index 000000000..5789ec2b8 --- /dev/null +++ b/plugins/french/wuxialnscantrad.ts @@ -0,0 +1,205 @@ +import { CheerioAPI, load } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class WuxialnscantradPlugin implements Plugin.PluginBase { + id = 'wuxialnscantrad'; + name = 'WuxiaLnScantrad'; + icon = 'src/fr/wuxialnscantrad/icon.png'; + site = 'https://wuxialnscantrad.wordpress.com'; + version = '1.0.0'; + + async getCheerio(url: string): Promise<CheerioAPI> { + const r = await fetchApi(url, { + headers: { 'Accept-Encoding': 'deflate' }, + }); + const body = await r.text(); + const $ = load(body); + return $; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + if (pageNo > 1) return []; + + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + const url = this.site; + const $ = await this.getCheerio(url); + $('#menu-item-2210 ul li').each((i, elem) => { + const novelName = $(elem).first().text().trim(); + const novelUrl = $(elem).find('a').attr('href'); + + if (novelUrl && novelName) { + novel = { + name: novelName, + cover: defaultCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = $('.entry-title').text().trim(); + novel.cover = + $('.entry-content p strong img').first().attr('src') || + $('.entry-content p img').first().attr('src'); + + const entryContentText = $('.entry-content').text(); + novel.author = this.getAuthor(entryContentText); + novel.genres = this.getGenres(entryContentText); + novel.artist = this.getArtist(entryContentText); + novel.summary = this.getSummary(entryContentText); + novel.status = this.getStatus(entryContentText); + + const pathChapter = $('.entry-content ul').first().children('li'); + const chapters: Plugin.ChapterItem[] = []; + pathChapter.each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).find('a').attr('href'); + if (chapterUrl && chapterUrl.includes(this.site) && chapterName) { + const pathchapter = chapterUrl.replace(this.site, ''); + // we do not take the paths already present + if (!chapters.some(chap => chap.path === pathchapter)) { + const releaseDate = dayjs( + chapterUrl?.substring(this.site.length + 1, this.site.length + 11), + ).format('DD MMMM YYYY'); + chapters.push({ + name: chapterName, + path: pathchapter, + releaseTime: releaseDate, + }); + } + } + }); + novel.chapters = chapters; + return novel; + } + + getAuthor(text: string) { + const regex = /Auteur\(s\):\s*(.*)/; + const match = regex.exec(text); + let author = ''; + if (match !== null) { + author = match[1].trim(); + } + return author; + } + + getGenres(text: string) { + const regex = /Genres:\s*(.*)/; + const match = regex.exec(text); + let genre = ''; + if (match !== null) { + genre = match[1].trim(); + } + return genre; + } + + getArtist(text: string) { + const regex = /Artiste\(s\):\s*(.*)Genres/; + const match = regex.exec(text); + let artist = ''; + if (match !== null) { + artist = match[1].trim(); + } + return artist; + } + + getSummary(text: string) { + const regexAuthors = [ + /Synopsis :([\s\S]*)Chapitres disponibles/, + /Sypnopsis([\s\S]*)Sypnopsis officiel/, + /Synopsis([\s\S]*)Chapitres disponibles/, + ]; + + for (const regex of regexAuthors) { + const match = regex.exec(text); + if (match !== null) { + return match[1].trim(); + } + } + return ''; + } + + getStatus(text: string) { + const regex = /Statut:\s*(.*)/; + const match = regex.exec(text); + let status = ''; + if (match !== null) { + status = match[1].trim(); + } + switch (status) { + case 'En cours': + return NovelStatus.Ongoing; + case 'Arrêté': + return NovelStatus.Cancelled; + case 'Terminé': + return NovelStatus.Completed; + default: + return NovelStatus.Ongoing; + } + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + + let contenuHtml = ''; + $('.entry-content') + .contents() + .each(function () { + if ($(this).html()?.includes('<script')) { + return false; + } + //Removing tags linked to navigation and unnecessary paragraphs. + if ( + !$(this).html()?.includes('data-attachment-id="480') && + !$.html(this)?.includes('<hr>') && + !$.html(this)?.includes('<p> </p>') + ) { + contenuHtml += $.html(this); + } + }); + return contenuHtml; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + const popularNovels = this.popularNovels(1); + + const novels = (await popularNovels).filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return novels; + } +} + +export default new WuxialnscantradPlugin(); diff --git a/plugins/french/xiaowaz.ts b/plugins/french/xiaowaz.ts new file mode 100644 index 000000000..5d3a09e50 --- /dev/null +++ b/plugins/french/xiaowaz.ts @@ -0,0 +1,299 @@ +import { Cheerio, CheerioAPI, load } from 'cheerio'; +import { Element } from 'domhandler'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class XiaowazPlugin implements Plugin.PluginBase { + id = 'xiaowaz'; + name = 'Xiaowaz'; + icon = 'src/fr/xiaowaz/icon.png'; + site = 'https://xiaowaz.fr'; + version = '1.0.2'; + static novels: Plugin.NovelItem[] | undefined; + + async getCheerio(url: string): Promise<CheerioAPI> { + let retries = 5; // when fetching for images the sites sometimes terminates the connection + let returnError: unknown; + while (retries > 0) { + try { + const r = await fetchApi(url); + const body = await r.text(); + const $ = load(body); + return $; + } catch (error) { + console.error(error); + returnError = error; + retries--; + // wait 1 second before retrying + await new Promise(resolve => setTimeout(resolve, 1000)); + } + } + throw new Error( + returnError ? String(returnError) : 'Error fetching the page', + ); + } + + async getAllNovels(): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + let novel: Plugin.NovelItem; + const $ = await this.getCheerio(this.site); + + const categories: Cheerio<Element>[] = [ + $('li.page_item').find('a:contains("Séries")').parent(), + $('li.page_item').find('a:contains("Créations")').parent(), + $('li.page_item').find('a:contains("†")').parent(), + ]; + + categories.forEach(cheerio => { + cheerio.find('ul.children li').each((i, elem) => { + const novelName = $(elem).first().text().trim().replace('✔', ''); + const novelUrl = $(elem).find('a').attr('href'); + + // Douluo Dalu is no good + if (novelUrl && novelName && novelName !== 'Douluo Dalu') { + novel = { + name: novelName, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + } + }); + }); + return novels; + } + + async getNovelsCovers( + novels: Plugin.NovelItem[], + ): Promise<Plugin.NovelItem[]> { + await Promise.all( + novels.map(async novel => { + const $novel = await this.getCheerio(this.site + novel.path); + novel.cover = + $novel('img[fetchpriority = "high"]').first().attr('src') || + $novel('img.aligncenter').first().attr('src') || + defaultCover; + }), + ); + return novels; + } + + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const PAGE_SIZE = 5; + + if (!XiaowazPlugin.novels) XiaowazPlugin.novels = await this.getAllNovels(); + let novels: Plugin.NovelItem[] = XiaowazPlugin.novels; + + const totalPages = Math.ceil(novels.length / PAGE_SIZE); + if (pageNo > totalPages) return []; + + // splitting novel list to make fewer requests for getting images + novels = novels.slice(PAGE_SIZE * (pageNo - 1), PAGE_SIZE * pageNo); + + return await this.getNovelsCovers(novels); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Sans titre', + }; + + const $ = await this.getCheerio(this.site + novelPath); + + novel.name = $('.card_title').text().trim(); + novel.cover = + $('img[fetchpriority = "high"]').first().attr('src') || + $('img.aligncenter').first().attr('src') || + defaultCover; + + novel.status = NovelStatus.Ongoing; + if (novel.name.charAt(novel.name.length - 1) === '✔') { + novel.status = NovelStatus.Completed; + novel.name = novel.name.slice(0, -1); + } else if (novelPath.startsWith('/series-abandonnees')) { + novel.status = NovelStatus.Cancelled; + } + + const entryContentText = $('.entry-content').text(); + novel.author = this.getAuthor(entryContentText); + novel.genres = this.getGenres(entryContentText); + if (novelPath.startsWith('/oeuvres-originales')) { + novel.genres += novel.genres ? ', Oeuvre originale' : 'Oeuvre originale'; + } + + const listeParagraphe: string[] = []; + + const PARAGRAPH_EXCLUDE_LIST = [ + 'Écrit par', + 'Ecrit par', + 'Sorties régulières', + 'Auteur\u00A0:', + 'Statut VO\u00A0:', + 'Nom utilisé\u00A0:', + 'Auteur original\u00A0:', + 'Auteur original de l’oeuvre', + '851 chapitres en', + 'Index\u00A0:', + ]; + + $('.entry-content > p').each((index, element) => { + const balise = $(element); + + // remove chapter links + if (balise.find('a[href*="xiaowaz.fr/articles"]').length !== 0) + return false; + + const textbalise = balise.text(); + if (PARAGRAPH_EXCLUDE_LIST.some(keyword => textbalise.includes(keyword))) + return false; + + if ( + !textbalise.includes('Genre') && + !textbalise.includes('Synopsis') && + //Managing the novel 'Rebirth of The Thief Who Roamed The World' to remove these three fields. + !textbalise.includes('重生之賊行天下') && + !textbalise.includes('Rebirth of The Thief Who Roamed The World') && + !textbalise.includes( + 'Romance, Comédie, Action, VRMMO, Réincarnation, Futuriste', + ) + ) + listeParagraphe.push(textbalise); + }); + novel.summary = listeParagraphe.join('\n').trim(); + + //Search for chapter links within ul/li tags; otherwise, within p/a tags, + //but not both at the same time because otherwise, on the TDG page, it redirects to a PDF download link. + let pathChapter = $('.entry-content ul li a'); + if (pathChapter.length === 0) { + pathChapter = $('.entry-content p a'); + } + + const chapters: Plugin.ChapterItem[] = []; + pathChapter.each((i, elem) => { + const chapterName = $(elem).text().trim(); + const chapterUrl = $(elem).attr('href'); + if (chapterUrl && chapterUrl.includes(this.site) && chapterName) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + } + }); + + novel.chapters = chapters; + return novel; + } + + getAuthor(text: string) { + const regexAuthors = [ + /Écrit par([^\n]*). Traduction/i, + /Écrit par([^\n]*)./i, + /Auteur original de l’œuvre\u00A0:([^\n]*)VO/i, + /Auteur\u00A0:([^\n]*)sur/, + /Auteur\u00A0:([^\n]*)/, + /Auteure\u00A0:\u00A0([^\n]*)/, + /Auteur original de l’oeuvre\u00A0:([^\n]*)/i, + /Auteur original\u00A0:([^\n]*)/i, + ]; + + for (const regex of regexAuthors) { + const match = regex.exec(text); + if (match !== null) { + return match[1].trim(); + } + } + + return ''; + } + + getGenres(text: string) { + // We handle several cases where there are multiple spellings for the word 'genre'. Genre, Genre :, Genres, Genres: + const regex = /Genre((?:.*?\n)*?)\s*Synopsis\s*/; + const match = regex.exec(text); + let genre = ''; + if (match !== null) { + genre = match[1] + .replace('\u00A0', ' ') + .replace('s :', '') + .replace(':', '') + .trim(); + if (genre.startsWith('s\n')) { + genre = genre.replace(/^./, '').trim(); + } + } + return genre; + } + + async parseChapter(chapterPath: string): Promise<string> { + const $ = await this.getCheerio(this.site + chapterPath); + + const startTag = $('.wp-post-navigation'); + const endTag = $('.abh_box.abh_box_down.abh_box_business'); + + const elementsBetweenTags: string[] = []; + let footnotesElement: string | null = null; + + if (startTag.length > 0 && endTag.length > 0) { + let currentElement = startTag.next(); + while (currentElement.length > 0 && !currentElement.is(endTag)) { + if ( + currentElement.find('p > a[href="https://ko-fi.com/wazouille"]') + .length > 0 + ) { + break; + } + if (currentElement.hasClass('footnote_container_prepare')) { + footnotesElement = $.html(currentElement); + } else { + let htmlCurrentElement = $.html(currentElement); + htmlCurrentElement = + htmlCurrentElement === '<p> </p>' + ? '<p/>' + : htmlCurrentElement; + elementsBetweenTags.push(htmlCurrentElement); + } + currentElement = currentElement.next(); + } + } + + // Place footnotes at the end of the chapter, not at the beginning + if (footnotesElement) { + elementsBetweenTags.push(footnotesElement); + } + + return elementsBetweenTags.join('\n').trim(); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + + if (!XiaowazPlugin.novels) XiaowazPlugin.novels = await this.getAllNovels(); + const popularNovels = XiaowazPlugin.novels; + + // Normalize the text to remove accents and other special characters + // This ensures that the search term and novel names are compared accurately + const novels = popularNovels.filter(novel => + novel.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim() + .includes( + searchTerm + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .trim(), + ), + ); + + return await this.getNovelsCovers(novels); + } +} + +export default new XiaowazPlugin(); diff --git a/plugins/index.ts b/plugins/index.ts new file mode 100644 index 000000000..ab55480f3 --- /dev/null +++ b/plugins/index.ts @@ -0,0 +1,501 @@ +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/Markazriwayat[madara]'; +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/Riwyat[madara]'; +import p_10 from '@plugins/arabic/dilartube'; +import p_11 from '@plugins/arabic/rewayatclub'; +import p_12 from '@plugins/arabic/sunovels'; +import p_13 from '@plugins/chinese/69shu'; +import p_14 from '@plugins/chinese/Quanben'; +import p_15 from '@plugins/chinese/ixdzs8'; +import p_16 from '@plugins/chinese/linovel'; +import p_17 from '@plugins/chinese/linovelib'; +import p_18 from '@plugins/chinese/linovelib_tw'; +import p_19 from '@plugins/chinese/novel543'; +import p_20 from '@plugins/english/AllNovelFull[readnovelfull]'; +import p_21 from '@plugins/english/AllNovel[readnovelfull]'; +import p_22 from '@plugins/english/ArcaneTranslations[lightnovelwp]'; +import p_23 from '@plugins/english/BelleReservoir[madara]'; +import p_24 from '@plugins/english/BoxNovel[madara]'; +import p_25 from '@plugins/english/CPUnovel[lightnovelwp]'; +import p_26 from '@plugins/english/CherryMistCafe[fictioneer]'; +import p_27 from '@plugins/english/CitrusAurora[madara]'; +import p_28 from '@plugins/english/CoralBoutique[madara]'; +import p_29 from '@plugins/english/DaoNovel[madara]'; +import p_30 from '@plugins/english/DaoTranslate[lightnovelwp]'; +import p_31 from '@plugins/english/DaoistQuest[fictioneer]'; +import p_32 from '@plugins/english/DearestRosalie[fictioneer]'; +import p_33 from '@plugins/english/DragonTea[madara]'; +import p_34 from '@plugins/english/Dragonholic[madara]'; +import p_35 from '@plugins/english/DuskBlossoms[madara]'; +import p_36 from '@plugins/english/ElloTL[lightnovelwp]'; +import p_37 from '@plugins/english/Eternalune[madara]'; +import p_38 from '@plugins/english/EtudeTranslations[madara]'; +import p_39 from '@plugins/english/FanNovel[readwn]'; +import p_40 from '@plugins/english/FansMTL[readwn]'; +import p_41 from '@plugins/english/FansTranslations[madara]'; +import p_42 from '@plugins/english/FirstKissNovel[madara]'; +import p_43 from '@plugins/english/Foxaholic[madara]'; +import p_44 from '@plugins/english/FreeWebNovel[readnovelfull]'; +import p_45 from '@plugins/english/GalaxyTranslations[madara]'; +import p_46 from '@plugins/english/Guavaread[madara]'; +import p_47 from '@plugins/english/HiraethTranslation[madara]'; +import p_48 from '@plugins/english/HotNovelPub[hotnovelpub]'; +import p_49 from '@plugins/english/Ippotranslations[lightnovelwp]'; +import p_50 from '@plugins/english/KDTNovels[lightnovelwp]'; +import p_51 from '@plugins/english/KeopiTranslations[lightnovelwp]'; +import p_52 from '@plugins/english/KnoxT[lightnovelwp]'; +import p_53 from '@plugins/english/LazyGirlTranslations[lightnovelwp]'; +import p_54 from '@plugins/english/LibRead[readnovelfull]'; +import p_55 from '@plugins/english/LightNovelCave[lightnovelworld]'; +import p_56 from '@plugins/english/LightNovelHeaven[madara]'; +import p_57 from '@plugins/english/LightNovelPlus[readnovelfull]'; +import p_58 from '@plugins/english/LightNovelPubVip[lightnovelworld]'; +import p_59 from '@plugins/english/LightNovelUpdates[madara]'; +import p_60 from '@plugins/english/LilyontheValley[fictioneer]'; +import p_61 from '@plugins/english/Ltnovel[readwn]'; +import p_62 from '@plugins/english/LulloBox[madara]'; +import p_63 from '@plugins/english/LunarLetters[madara]'; +import p_64 from '@plugins/english/MTLNovel[madara]'; +import p_65 from '@plugins/english/MTLNovel[mtlnovel]'; +import p_66 from '@plugins/english/Meownovel[madara]'; +import p_67 from '@plugins/english/MoonlightNovels[lightnovelwp]'; +import p_68 from '@plugins/english/MostNovel[madara]'; +import p_69 from '@plugins/english/MysticalSeries[madara]'; +import p_70 from '@plugins/english/NeoSekaiTranslations[madara]'; +import p_71 from '@plugins/english/NitroManga[madara]'; +import p_72 from '@plugins/english/NobleMTL[lightnovelwp]'; +import p_73 from '@plugins/english/NoiceTranslations[madara]'; +import p_74 from '@plugins/english/NovelBin[readnovelfull]'; +import p_75 from '@plugins/english/NovelCool[novelcool]'; +import p_76 from '@plugins/english/NovelFull[readnovelfull]'; +import p_77 from '@plugins/english/NovelLib[fictioneer]'; +import p_78 from '@plugins/english/NovelMultiverse[madara]'; +import p_79 from '@plugins/english/NovelNinja[madara]'; +import p_80 from '@plugins/english/NovelOnline'; +import p_81 from '@plugins/english/NovelTranslate[madara]'; +import p_82 from '@plugins/english/NovelsKnight[lightnovelwp]'; +import p_83 from '@plugins/english/PandaMachineTranslations[lightnovelwp]'; +import p_84 from '@plugins/english/PastelTales[madara]'; +import p_85 from '@plugins/english/PenguinSquad[fictioneer]'; +import p_86 from '@plugins/english/Prizma[fictioneer]'; +import p_87 from '@plugins/english/Ranobes[ranobes]'; +import p_88 from '@plugins/english/Ranovel[madara]'; +import p_89 from '@plugins/english/ReadFanfic[madara]'; +import p_90 from '@plugins/english/ReadNovelFull[readnovelfull]'; +import p_91 from '@plugins/english/RequiemTranslations[lightnovelwp]'; +import p_92 from '@plugins/english/SalmonLatte[madara]'; +import p_93 from '@plugins/english/SleepyTranslations[madara]'; +import p_94 from '@plugins/english/SonicMTL[madara]'; +import p_95 from '@plugins/english/SrankManga[madara]'; +import p_96 from '@plugins/english/StorySeedling'; +import p_97 from '@plugins/english/SweetEscape[madara]'; +import p_98 from '@plugins/english/SystemTranslation[lightnovelwp]'; +import p_99 from '@plugins/english/TranslatinOtaku[madara]'; +import p_100 from '@plugins/english/TranslationWeaver[lightnovelwp]'; +import p_101 from '@plugins/english/UniversalNovel[lightnovelwp]'; +import p_102 from '@plugins/english/VandyTranslate[lightnovelwp]'; +import p_103 from '@plugins/english/VioletLily[madara]'; +import p_104 from '@plugins/english/WebNovelLover[madara]'; +import p_105 from '@plugins/english/WebNovelPub[lightnovelworld]'; +import p_106 from '@plugins/english/WebNovelTranslation[madara]'; +import p_107 from '@plugins/english/WhiteMoonlightNovels[lightnovelwp]'; +import p_108 from '@plugins/english/WooksTeahouse[madara]'; +import p_109 from '@plugins/english/WordExcerpt[madara]'; +import p_110 from '@plugins/english/WuxiaSpace[readwn]'; +import p_111 from '@plugins/english/WuxiaV[readwn]'; +import p_112 from '@plugins/english/WuxiaWorldSite[madara]'; +import p_113 from '@plugins/english/Wuxiabox[readwn]'; +import p_114 from '@plugins/english/Wuxiafox[readwn]'; +import p_115 from '@plugins/english/ZetroTranslation[madara]'; +import p_116 from '@plugins/english/ao3'; +import p_117 from '@plugins/english/chrysanthemumgarden'; +import p_118 from '@plugins/english/crimsonscrolls'; +import p_119 from '@plugins/english/divinedaolibrary'; +import p_120 from '@plugins/english/dreambigtl'; +import p_121 from '@plugins/english/faqwikius'; +import p_122 from '@plugins/english/fenrirrealm'; +import p_123 from '@plugins/english/fictionzone'; +import p_124 from '@plugins/english/foxteller'; +import p_125 from '@plugins/english/genesis'; +import p_126 from '@plugins/english/indraTranslations'; +import p_127 from '@plugins/english/inkitt'; +import p_128 from '@plugins/english/inoveltranslation'; +import p_129 from '@plugins/english/leafstudio'; +import p_130 from '@plugins/english/lightnoveltranslation'; +import p_131 from '@plugins/english/lnmtl'; +import p_132 from '@plugins/english/mvlempyr'; +import p_133 from '@plugins/english/novelbuddy'; +import p_134 from '@plugins/english/novelfire'; +import p_135 from '@plugins/english/novelhall'; +import p_136 from '@plugins/english/novelhi'; +import p_137 from '@plugins/english/novelight'; +import p_138 from '@plugins/english/novelrest'; +import p_139 from '@plugins/english/novelupdates'; +import p_140 from '@plugins/english/pawread'; +import p_141 from '@plugins/english/rainofsnow'; +import p_142 from '@plugins/english/readfrom'; +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/wct'; +import p_148 from '@plugins/english/webnovel'; +import p_149 from '@plugins/english/wntl'; +import p_150 from '@plugins/english/wtrlab'; +import p_151 from '@plugins/english/wuxiaworld'; +import p_152 from '@plugins/french/LighNovelFR[lightnovelwp]'; +import p_153 from '@plugins/french/MTLNovel(FR)[mtlnovel]'; +import p_154 from '@plugins/french/MassNovel[madara]'; +import p_155 from '@plugins/french/WorldNovel[madara]'; +import p_156 from '@plugins/french/chireads'; +import p_157 from '@plugins/french/harkeneliwood'; +import p_158 from '@plugins/french/kisswood'; +import p_159 from '@plugins/french/noveldeglace'; +import p_160 from '@plugins/french/novhell'; +import p_161 from '@plugins/french/warriorlegendtrad'; +import p_162 from '@plugins/french/wuxialnscantrad'; +import p_163 from '@plugins/french/xiaowaz'; +import p_164 from '@plugins/indonesian/BacaLightNovel[lightnovelwp]'; +import p_165 from '@plugins/indonesian/MTLNovel(ID)[mtlnovel]'; +import p_166 from '@plugins/indonesian/MeioNovel[madara]'; +import p_167 from '@plugins/indonesian/NovelBookID[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/sakuranovel'; +import p_173 from '@plugins/japanese/Syosetu'; +import p_174 from '@plugins/japanese/kakuyomu'; +import p_175 from '@plugins/korean/Agitoon'; +import p_176 from '@plugins/korean/FortuneEternal[madara]'; +import p_177 from '@plugins/multi/komga'; +import p_178 from '@plugins/polish/novelki'; +import p_179 from '@plugins/portuguese/BetterNovels[lightnovelwp]'; +import p_180 from '@plugins/portuguese/CentralNovel[lightnovelwp]'; +import p_181 from '@plugins/portuguese/Kiniga[madara]'; +import p_182 from '@plugins/portuguese/LaNovels[hotnovelpub]'; +import p_183 from '@plugins/portuguese/LightNovelBrasil[lightnovelwp]'; +import p_184 from '@plugins/portuguese/MTLNovel(PT)[mtlnovel]'; +import p_185 from '@plugins/portuguese/blogdoamonnovels'; +import p_186 from '@plugins/portuguese/illusia'; +import p_187 from '@plugins/portuguese/novelmania'; +import p_188 from '@plugins/portuguese/tsundoku'; +import p_189 from '@plugins/russian/Bllate[rulate]'; +import p_190 from '@plugins/russian/Bookhamster[ifreedom]'; +import p_191 from '@plugins/russian/Erolate[rulate]'; +import p_192 from '@plugins/russian/EzNovels[hotnovelpub]'; +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/novelTL'; +import p_203 from '@plugins/russian/ranobehub'; +import p_204 from '@plugins/russian/ranobelib'; +import p_205 from '@plugins/russian/ranoberf'; +import p_206 from '@plugins/russian/renovels'; +import p_207 from '@plugins/russian/topliba'; +import p_208 from '@plugins/russian/zelluloza'; +import p_209 from '@plugins/russian/СвободныйМирРанобэ[ifreedom]'; +import p_210 from '@plugins/spanish/AllNovelRead[lightnovelwp]'; +import p_211 from '@plugins/spanish/AnimesHoy12[madara]'; +import p_212 from '@plugins/spanish/LightNovelDaily[hotnovelpub]'; +import p_213 from '@plugins/spanish/MTLNovel(ES)[mtlnovel]'; +import p_214 from '@plugins/spanish/NOVA'; +import p_215 from '@plugins/spanish/PanchoTranslations[madara]'; +import p_216 from '@plugins/spanish/TC&Sega[lightnovelwp]'; +import p_217 from '@plugins/spanish/TraduccionesAmistosas[madara]'; +import p_218 from '@plugins/spanish/hasutl'; +import p_219 from '@plugins/spanish/novelasligera'; +import p_220 from '@plugins/spanish/novelawuxia'; +import p_221 from '@plugins/spanish/novelyra'; +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/vietnamese/LNHako'; +import p_245 from '@plugins/vietnamese/lightnovelvn'; +import p_246 from '@plugins/vietnamese/nettruyen'; +import p_247 from '@plugins/vietnamese/truyenss'; + +const PLUGINS: Plugin.PluginBase[] = [ + p_0, + p_1, + p_2, + p_3, + p_4, + p_5, + p_6, + p_7, + p_8, + p_9, + p_10, + p_11, + p_12, + p_13, + p_14, + p_15, + p_16, + p_17, + p_18, + p_19, + p_20, + p_21, + p_22, + p_23, + p_24, + p_25, + p_26, + p_27, + p_28, + p_29, + p_30, + p_31, + p_32, + p_33, + p_34, + p_35, + p_36, + p_37, + p_38, + p_39, + p_40, + p_41, + p_42, + p_43, + p_44, + p_45, + p_46, + p_47, + p_48, + p_49, + p_50, + p_51, + p_52, + p_53, + p_54, + p_55, + p_56, + p_57, + p_58, + p_59, + p_60, + p_61, + p_62, + p_63, + p_64, + p_65, + p_66, + p_67, + p_68, + p_69, + p_70, + p_71, + p_72, + p_73, + p_74, + p_75, + p_76, + p_77, + p_78, + p_79, + p_80, + p_81, + p_82, + p_83, + p_84, + p_85, + p_86, + p_87, + p_88, + p_89, + p_90, + p_91, + p_92, + p_93, + p_94, + p_95, + p_96, + p_97, + p_98, + p_99, + p_100, + p_101, + p_102, + p_103, + p_104, + p_105, + p_106, + p_107, + p_108, + p_109, + p_110, + p_111, + p_112, + p_113, + p_114, + p_115, + p_116, + p_117, + p_118, + p_119, + p_120, + p_121, + p_122, + p_123, + p_124, + p_125, + p_126, + p_127, + p_128, + p_129, + p_130, + p_131, + p_132, + p_133, + p_134, + p_135, + p_136, + p_137, + p_138, + p_139, + p_140, + p_141, + p_142, + p_143, + p_144, + p_145, + p_146, + p_147, + p_148, + p_149, + p_150, + p_151, + p_152, + p_153, + p_154, + p_155, + p_156, + p_157, + p_158, + p_159, + p_160, + p_161, + p_162, + p_163, + p_164, + p_165, + p_166, + p_167, + p_168, + p_169, + p_170, + p_171, + p_172, + p_173, + p_174, + p_175, + p_176, + p_177, + p_178, + p_179, + p_180, + p_181, + p_182, + p_183, + p_184, + p_185, + p_186, + p_187, + p_188, + p_189, + p_190, + p_191, + p_192, + p_193, + p_194, + p_195, + p_196, + p_197, + p_198, + p_199, + p_200, + p_201, + p_202, + p_203, + p_204, + p_205, + p_206, + p_207, + p_208, + p_209, + p_210, + p_211, + p_212, + p_213, + p_214, + p_215, + p_216, + p_217, + p_218, + p_219, + p_220, + p_221, + p_222, + p_223, + p_224, + p_225, + p_226, + p_227, + p_228, + p_229, + p_230, + 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, +]; +export default PLUGINS; diff --git a/plugins/indonesian/indowebnovel.ts b/plugins/indonesian/indowebnovel.ts new file mode 100644 index 000000000..121d4d651 --- /dev/null +++ b/plugins/indonesian/indowebnovel.ts @@ -0,0 +1,223 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +// import { Filters, FilterTypes } from '@libs/filterInputs'; + +class IndoWebNovel implements Plugin.PluginBase { + id = 'IDWN.id'; + name = 'IndoWebNovel'; + icon = 'src/id/indowebnovel/icon.png'; + site = 'https://indowebnovel.id/'; + version = '1.2.3'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.flexbox2-item').each((i, el) => { + const novelName = loadedCheerio(el) + .find('.flexbox2-title span') + .first() + .text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el) + .find('.flexbox2-content > a') + .attr('href'); + + if (!novelUrl) return; + + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.slice(this.site.length), + }); + }); + + return novels; + } + + async popularNovels( + page = 1, + // { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + /* + let link = `${this.site}advanced-search/page/${page}/?title=&author=&yearx=`; + link += `&status=${filters.status.value}`; + link += `&type=${filters.type.value}`; + link += `&order=${filters.sort.value}`; + + if (filters.lang.value.length) + link += filters.lang.value.map(i => `&country[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); + */ + const link = this.site + `page/${page}/?s`; + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + loadedCheerio('.series-synops div').remove(); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.series-title h2').text().trim() || 'Untitled', + cover: loadedCheerio('.series-thumb img').attr('src'), + author: loadedCheerio(".series-infolist li:contains('Author') span") + .text() + .trim(), + status: + loadedCheerio('.status').text().trim() === 'Completed' + ? NovelStatus.Completed + : NovelStatus.Ongoing, + summary: loadedCheerio('.series-synops').text().trim(), + chapters: [], + }; + + novel.genres = loadedCheerio('.series-genres a') + .map((i, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(','); + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.series-chapterlist li').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text().trim(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + + chapters.push({ + name: chapterName, + path: chapterUrl.slice(this.site.length), + }); + }); + + novel.chapters = chapters.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('.adsads').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page = 1, + ): Promise<Plugin.NovelItem[]> { + /* + let link = `${this.site}advanced-search/page/${page}/?title=${searchTerm}&author=&yearx=`; + link += `&status=${this.filters.status.value}`; + link += `&type=${this.filters.type.value}`; + link += `&order=${this.filters.sort.value}`; + link += this.filters.lang.value.map(i => `&country[]=${i}`).join(''); + */ + const link = this.site + `page/${page}/?s=${searchTerm}`; + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + // filters = { + // status: { + // value: '', + // label: 'Status', + // options: [ + // { label: 'All', value: '' }, + // { label: 'Ongoing', value: 'ongoing' }, + // { label: 'Completed', value: 'completed' }, + // ], + // type: FilterTypes.Picker, + // }, + // type: { + // value: '', + // label: 'Type', + // options: [ + // { label: 'All', value: '' }, + // { label: 'Web Novel', value: 'Web+Novel' }, + // { label: 'Light Novel', value: 'Light+Novel' }, + // ], + // type: FilterTypes.Picker, + // }, + // sort: { + // value: 'rating', + // label: 'Order By', + // options: [ + // { label: 'A-Z', value: 'title' }, + // { label: 'Z-A', value: 'titlereverse' }, + // { label: 'Latest Update', value: 'update' }, + // { label: 'Latest Added', value: 'latest' }, + // { label: 'Popular', value: 'popular' }, + // { label: 'Rating', value: 'rating' }, + // ], + // type: FilterTypes.Picker, + // }, + // lang: { + // value: ['china', 'jepang', 'korea', 'unknown'], + // label: 'Country', + // options: [ + // { label: 'China', value: 'china' }, + // { label: 'Jepang', value: 'jepang' }, + // { label: 'Korea', value: 'korea' }, + // { label: 'Unknown', value: 'unknown' }, + // ], + // type: FilterTypes.CheckboxGroup, + // }, + // genre: { + // value: [], + // label: 'Genres', + // options: [ + // { label: 'Action', value: 'action' }, + // { label: 'Adult', value: 'adult' }, + // { label: 'Adventure', value: 'adventure' }, + // { label: 'Comedy', value: 'comedy' }, + // { label: 'Drama', value: 'drama' }, + // { label: 'Ecchi', value: 'ecchi' }, + // { label: 'Fantasy', value: 'fantasy' }, + // { label: 'Gender Bender', value: 'gender-bender' }, + // { label: 'Harem', value: 'harem' }, + // { label: 'Horror', value: 'horror' }, + // { label: 'Josei', value: 'josei' }, + // { label: 'Josei', value: 'josei' }, + // { label: 'Martial Arts', value: 'martial-arts' }, + // { label: 'Mature', value: 'mature' }, + // { label: 'Mecha', value: 'mecha' }, + // { label: 'Mystery', value: 'mystery' }, + // { label: 'Psychological', value: 'psychological' }, + // { label: 'Romance', value: 'romance' }, + // { label: 'School Life', value: 'school-life' }, + // { label: 'Sci-fi', value: 'sci-fi' }, + // { label: 'Seinen', value: 'seinen' }, + // { label: 'Shoujo', value: 'shoujo' }, + // { label: 'Shounen', value: 'shounen' }, + // { label: 'Slice of Life', value: 'slice-of-life' }, + // { label: 'Smut', value: 'smut' }, + // { label: 'Supernatural', value: 'supernatural' }, + // { label: 'Tragedy', value: 'tragedy' }, + // { label: 'Wuxia', value: 'wuxia' }, + // { label: 'Xianxia', value: 'xianxia' }, + // { label: 'Xuanhuan', value: 'xuanhuan' }, + // ], + // type: FilterTypes.CheckboxGroup, + // }, + // } satisfies Filters; +} + +export default new IndoWebNovel(); diff --git a/plugins/indonesian/novelringan.broken.ts b/plugins/indonesian/novelringan.broken.ts new file mode 100644 index 000000000..0e76ed364 --- /dev/null +++ b/plugins/indonesian/novelringan.broken.ts @@ -0,0 +1,256 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class NovelRingan implements Plugin.PluginBase { + id = 'novelringan.com'; + name = 'NovelRingan'; + icon = 'src/id/novelringan/icon.png'; + site = 'https://novelringan.com/'; + version = '1.0.0'; + coverUriPrefix = 'https://i0.wp.com/novelringan.com/wp-content/uploads/'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.post').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.entry-title').text()?.trim(); + const novelCover = + this.coverUriPrefix + loadedCheerio(ele).find('img').attr('data-sxrx'); + const novelUrl = loadedCheerio(ele).find('h2 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = `${this.site}advanced-search/page/${page}/?title`; + link += `&status=${filters.status.value}`; + link += `&order=${filters.sort.value}`; + + if (filters.type.value.length) + link += filters.type.value.map((i: string) => `&type[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map((i: string) => `&genre[]=${i}`).join(''); + + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const styletag = Array.from( + loadedCheerio('meta[name=msapplication-TileImage] + style') + .html() + ?.matchAll(/"(.*?)"/g) || [], + m => m[1], + ); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: styletag[0] || loadedCheerio('.entry-title').text() || 'Untitled', + author: styletag[1], + summary: loadedCheerio('.maininfo span p').text(), + chapters: [], + }; + + novel.cover = + this.coverUriPrefix + + loadedCheerio('img.ts-post-image').attr('data-sxrx'); + + loadedCheerio('.maininfo li').each(function () { + const detailName = loadedCheerio(this).find('b').text().trim(); + const detail = loadedCheerio(this).find('b').remove().end().text().trim(); + + switch (detailName) { + case 'Status:': + novel.status = detail; + break; + case 'Genre:': + novel.genres = detail; + break; + } + }); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.bxcl > ul > li').each((i, el) => { + const chapterName = loadedCheerio(el).find('a').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + loadedCheerio('.entry-content div[style="display:none"]').remove(); + + const chapterText = loadedCheerio('.entry-content').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const url = this.site + 'page/' + page + '/?s=' + searchTerm; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'Ongoing' }, + { label: 'Completed', value: 'Completed' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'popular', + label: 'Urutkan', + options: [ + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Terbarui', value: 'update' }, + { label: 'Ditambahkan', value: 'latest' }, + { label: 'Terpopuler', value: 'popular' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: [], + label: 'Tipe', + options: [ + { label: 'Chinese Novel', value: 'chinese-novel' }, + { label: 'Chinese Web Novel', value: 'chinese-web-novel' }, + { label: 'Filipino Novel', value: 'filipino-novel' }, + { label: 'Indonesia Novel', value: 'indonesia-novel' }, + { label: 'Korean Novel', value: 'korean-novel' }, + { label: 'Light Novel', value: 'light-novel' }, + { label: 'Light Novel (CN)', value: 'light-novel-cn' }, + { label: 'Light Novel (JP)', value: 'light-novel-jp' }, + { label: 'Light Novel (KR)', value: 'light-novel-kr' }, + { label: 'Malaysian Novel', value: 'malaysian-novel' }, + { label: 'Published Novel (CN)', value: 'published-novel-cn' }, + { label: 'Published Novel (JP)', value: 'published-novel-jp' }, + { label: 'Published Novel (KR)', value: 'published-novel-kr' }, + { label: 'Published Novel (TH)', value: 'published-novel-th' }, + { label: 'Thai Novel', value: 'thai-novel' }, + { label: 'Vietnamese Novel', value: 'vietnamese-novel' }, + { label: 'Web Novel', value: 'web-novel' }, + { label: 'Webnovel', value: 'webnovel' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Celebrity', value: 'celebrity' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'ction', value: 'ction' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern', value: 'eastern' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Game', value: 'game' }, + { label: 'Games', value: 'games' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Historical', value: 'historical' }, + { label: 'Horror', value: 'horror' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Josei', value: 'josei' }, + { label: 'Life', value: 'life' }, + { label: 'LitRPG', value: 'litrpg' }, + { label: 'Magical Realism', value: 'magical-realism' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychologic', value: 'psychologic' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Recarnation', value: 'recarnation' }, + { label: 'Reincarnation', value: 'reincarnation' }, + { label: 'Romance', value: 'romance' }, + { label: 'School', value: 'school' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shotacon', value: 'shotacon' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Sports', value: 'sports' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Urban', value: 'urban' }, + { label: 'Urban Life', value: 'urban-life' }, + { + label: 've names:N/A Genre:Romance', + value: 've-namesn-a-genreromance', + }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new NovelRingan(); diff --git a/plugins/indonesian/sakuranovel.ts b/plugins/indonesian/sakuranovel.ts new file mode 100644 index 000000000..6200861cb --- /dev/null +++ b/plugins/indonesian/sakuranovel.ts @@ -0,0 +1,247 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class SakuraNovel implements Plugin.PluginBase { + id = 'sakura.id'; + name = 'SakuraNovel'; + icon = 'src/id/sakuranovel/icon.png'; + site = 'https://sakuranovel.id/'; + version = '1.0.1'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.flexbox2-item').each((i, el) => { + const novelName = loadedCheerio(el) + .find('.flexbox2-title span') + .first() + .text(); + const novelCover = loadedCheerio(el).find('img').attr('src'); + const novelUrl = loadedCheerio(el) + .find('.flexbox2-content > a') + .attr('href'); + + if (!novelUrl) return; + + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = `${this.site}advanced-search/page/${page}/?title&author&yearx`; + link += `&status=${filters.status.value}`; + link += `&type=${filters.type.value}`; + link += `&order=${filters.sort.value}`; + + if (filters.lang.value.length) + link += filters.lang.value.map(i => `&country[]=${i}`).join(''); + if (filters.genre.value.length) + link += filters.genre.value.map(i => `&genre[]=${i}`).join(''); + + const result = await fetchApi(link); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + loadedCheerio('.series-synops div').remove(); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.series-title h2').text().trim() || 'Untitled', + cover: loadedCheerio('.series-thumb img').attr('src'), + author: loadedCheerio(".series-infolist > li b:contains('Author') +") + .text() + .trim(), + status: loadedCheerio('.status').text().trim(), + summary: loadedCheerio('.series-synops').text().trim(), + chapters: [], + }; + + novel.genres = loadedCheerio('.series-genres') + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + const imageTitle = novel.cover + ?.split('/') + .pop() + ?.split('-') + .join(' ') + .split('.')[0]; + const chapterTitle = novel.name + .replace(/\(LN\)|\(WN\)/, '') + .split(',')[0] + .trim(); + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('.series-flexright li').each((i, el) => { + const chapterName = loadedCheerio(el) + .find('a span') + .first() + .text() + .replace(chapterTitle, '') + .replace(imageTitle!, '') + .replace(/Bahasa Indonesia/, '') + .replace(/\s+/g, ' ') + .trim(); + + const releaseDate = loadedCheerio(el) + .find('.date') + .text() + .trim() + .split('/') + .map(x => Number(x)); + + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + releaseTime: new Date( + releaseDate[2], + releaseDate[1], + releaseDate[0], + ).toISOString(), + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapter.reverse(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const divi = loadedCheerio("div:contains('Daftar Isi') +") + .find('div:first') + .attr('class'); + loadedCheerio(`.${divi}`).remove(); + const chapterText = + loadedCheerio("div:contains('Daftar Isi') +").html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}page/${page}/?s=${searchTerm}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Ongoing', value: 'ongoing' }, + { label: 'Completed', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + type: { + value: '', + label: 'Type', + options: [ + { label: 'All', value: '' }, + { label: 'Web Novel', value: 'Web+Novel' }, + { label: 'Light Novel', value: 'Light+Novel' }, + ], + type: FilterTypes.Picker, + }, + sort: { + value: 'rating', + label: 'Order By', + options: [ + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Latest Update', value: 'update' }, + { label: 'Latest Added', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + { label: 'Rating', value: 'rating' }, + ], + type: FilterTypes.Picker, + }, + lang: { + value: ['china', 'jepang', 'korea', 'unknown'], + label: 'Country', + options: [ + { label: 'China', value: 'china' }, + { label: 'Jepang', value: 'jepang' }, + { label: 'Korea', value: 'korea' }, + { label: 'Unknown', value: 'unknown' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genre: { + value: [], + label: 'Genres', + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Josei', value: 'josei' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Romance', value: 'romance' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Smut', value: 'smut' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new SakuraNovel(); diff --git a/plugins/japanese/Syosetu.ts b/plugins/japanese/Syosetu.ts new file mode 100644 index 000000000..30425ef99 --- /dev/null +++ b/plugins/japanese/Syosetu.ts @@ -0,0 +1,345 @@ +import { CheerioAPI, load as loadCheerio } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +// const novelStatus = require('@/types/constants'); +// const isUrlAbsolute = require('@/lib/utils'); +// const parseDate = require('@libs/parseDate'); + +class Syosetu implements Plugin.PluginBase { + id = 'yomou.syosetu'; + name = 'Syosetu'; + icon = 'src/jp/syosetu/icon.png'; + site = 'https://yomou.syosetu.com/'; + novelPrefix = 'https://ncode.syosetu.com'; + version = '1.1.4'; + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }; + searchUrl = (pagenum?: number, order?: string) => { + return `${this.site}search.php?order=${order || 'hyoka'}${ + pagenum !== undefined + ? `&p=${pagenum <= 1 || pagenum > 100 ? '1' : pagenum}` // check if pagenum is between 1 and 100 + : '' // if isn't don't set ?p + }`; + }; + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const getNovelsFromPage = async (pagenumber: number) => { + // load page + let url = this.site; + + if (!filters.genre.value) { + url += `rank/list/type/${filters.ranking.value}_${filters.modifier.value}/?p=${pagenumber}`; + } else { + url += `rank/${ + filters.genre.value.length === 1 ? 'isekailist' : 'genrelist' + }/type/${filters.ranking.value}_${filters.genre.value}${ + filters.modifier.value === 'total' ? '' : `_${filters.modifier.value}` + }/?p=${pagenumber}`; + } + const html = await (await fetchApi(url)).text(); + + const loadedCheerio = loadCheerio(html); + + if (parseInt(loadedCheerio('.is-current').html() || '1') !== pagenumber) + return []; + + const novels: Plugin.NovelItem[] = []; + loadedCheerio('.c-card').each((_, e) => { + const anchor = loadedCheerio(e).find('.p-ranklist-item__title a'); + const url = anchor.attr('href'); + if (!url) return; + const name = anchor.text(); + const novel: Plugin.NovelItem = { + path: url.replace(this.novelPrefix, ''), + name, + cover: defaultCover, + }; + novels.push(novel); + }); + return novels; + }; + const novels = await getNovelsFromPage(pageNo); + return novels; + } + private async parseChaptersFromPage( + loadedCheerio: CheerioAPI, + ): Promise<Plugin.ChapterItem[]> { + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.p-eplist__sublist').each((_, element) => { + const chapterLink = loadedCheerio(element).find('a'); + const chapterUrl = chapterLink.attr('href'); + const chapterName = chapterLink.text().trim(); + const releaseDate = loadedCheerio(element) + .find('.p-eplist__update') + .text() + .trim() + .split(' ')[0] + .replace(/\//g, '-'); + + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.novelPrefix, ''), + }); + } + }); + + return chapters; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + // First fetch main page + const result = await fetchApi(this.novelPrefix + novelPath, { + headers: this.headers, + }); + const body = await result.text(); + const loadedCheerio = loadCheerio(body); + + // Parse status + let status = 'Unknown'; + if ( + loadedCheerio('.c-announce').text().includes('連載中') || + loadedCheerio('.c-announce').text().includes('未完結') + ) { + status = NovelStatus.Ongoing; + } else if ( + loadedCheerio('.c-announce').text().includes('更新されていません') + ) { + status = NovelStatus.OnHiatus; + } else if (loadedCheerio('.c-announce').text().includes('完結')) { + status = NovelStatus.Completed; + } + + // Create novel object with metadata + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.p-novel__title').text(), + author: loadedCheerio('.p-novel__author') + .text() + .replace('作者:', '') + .trim(), + status: status, + artist: '', + cover: defaultCover, + chapters: [], + genres: loadedCheerio('meta[property="og:description"]') + .attr('content') + ?.split(' ') + .join(','), // Get genres from meta tag + }; + + // Get summary if available + novel.summary = loadedCheerio('#novel_ex').html() || ''; + + const chapters: Plugin.ChapterItem[] = []; + + // Get last page URL first + const lastPageLink = loadedCheerio('.c-pager__item--last').attr('href'); + + if (!lastPageLink) { + // If no pagination, just parse chapters from the current page + loadedCheerio('.p-eplist__sublist').each((_, element) => { + const chapterLink = loadedCheerio(element).find('a'); + const chapterUrl = chapterLink.attr('href'); + const chapterName = chapterLink.text().trim(); + const releaseDate = loadedCheerio(element) + .find('.p-eplist__update') + .text() + .trim() + .split(' ')[0] + .replace(/\//g, '-'); + + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.novelPrefix, ''), + }); + } + }); + } else { + const lastPageMatch = lastPageLink.match(/\?p=(\d+)/); + const totalPages = lastPageMatch ? parseInt(lastPageMatch[1]) : 1; + + // Fetch all pages in parallel for better performance + const pagePromises = Array.from({ length: totalPages }, (_, i) => + fetchApi(`${this.novelPrefix}${novelPath}?p=${i + 1}`).then(r => + r.text(), + ), + ); + + const pageResults = await Promise.all(pagePromises); + + // Process each page's chapters + pageResults.forEach(pageBody => { + const pageCheerio = loadCheerio(pageBody); + pageCheerio('.p-eplist__sublist').each((_, element) => { + const chapterLink = pageCheerio(element).find('a'); + const chapterUrl = chapterLink.attr('href'); + const chapterName = chapterLink.text().trim(); + const releaseDate = pageCheerio(element) + .find('.p-eplist__update') + .text() + .trim() + .split(' ')[0] + .replace(/\//g, '-'); + + if (chapterUrl) { + chapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.novelPrefix, ''), + }); + } + }); + }); + } + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.novelPrefix + chapterPath, { + headers: this.headers, + }); + const body = await result.text(); + + const cheerioQuery = loadCheerio(body); + + // Get the chapter title + const chapterTitle = cheerioQuery('.p-novel__title').html() || ''; + + // Get the chapter content + const chapterContent = + cheerioQuery( + '.p-novel__body .p-novel__text:not([class*="p-novel__text--"])', + ).html() || ''; + + // Combine title and content with proper HTML structure + return `<h1>${chapterTitle}</h1>${chapterContent}`; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + let novels = []; + + // returns list of novels from given page + const getNovelsFromPage = async (pagenumber: number) => { + // load page + const url = this.searchUrl(pagenumber) + `&word=${searchTerm}`; + const result = await fetchApi(url, { headers: this.headers }); + const body = await result.text(); + // Cheerio it! + const cheerioQuery = loadCheerio(body); + + const pageNovels: Plugin.NovelItem[] = []; + // find class=searchkekka_box + cheerioQuery('.searchkekka_box').each((i, e) => { + // get div with link and name + const novelDIV = cheerioQuery(e).find('.novel_h'); + // get link element + const novelA = novelDIV.children()[0]; + // add new novel to array + const novelPath = novelA.attribs.href.replace(this.novelPrefix, ''); + if (novelPath) { + pageNovels.push({ + name: novelDIV.text(), // get the name + path: novelPath, // get last part of the link + cover: defaultCover, + }); + } + }); + // return all novels from this page + return pageNovels; + }; + + // counter of loaded pages + // let pagesLoaded = 0; + // do { + // // always load first one + // novels.push(...(await getNovelsFromPage(pagesLoaded + 1))); + // pagesLoaded++; + // } while (pagesLoaded < maxPageLoad && isNext); // check if we should load more + + novels = await getNovelsFromPage(pageNo); + + /** Use + * novels.push(...(await getNovelsFromPage(pageNumber))) + * if you want to load more + */ + + // respond with novels! + return novels; + } + + resolveUrl(path: string): string { + return this.novelPrefix + path; + } + filters = { + ranking: { + type: FilterTypes.Picker, + label: 'Ranked by', + options: [ + { label: '日間', value: 'daily' }, + { label: '週間', value: 'weekly' }, + { label: '月間', value: 'monthly' }, + { label: '四半期', value: 'quarter' }, + { label: '年間', value: 'yearly' }, + { label: '累計', value: 'total' }, + ], + value: 'total', + }, + genre: { + type: FilterTypes.Picker, + label: 'Ranking Genre', + options: [ + { label: '総ジャンル', value: '' }, + { label: '異世界転生/転移〔恋愛〕〕', value: '1' }, + { label: '異世界転生/転移〔ファンタジー〕', value: '2' }, + { label: '異世界転生/転移〔文芸・SF・その他〕', value: 'o' }, + { label: '異世界〔恋愛〕', value: '101' }, + { label: '現実世界〔恋愛〕', value: '102' }, + { label: 'ハイファンタジー〔ファンタジー〕', value: '201' }, + { label: 'ローファンタジー〔ファンタジー〕', value: '202' }, + { label: '純文学〔文芸〕', value: '301' }, + { label: 'ヒューマンドラマ〔文芸〕', value: '302' }, + { label: '歴史〔文芸〕', value: '303' }, + { label: '推理〔文芸〕', value: '304' }, + { label: 'ホラー〔文芸〕', value: '305' }, + { label: 'アクション〔文芸〕', value: '306' }, + { label: 'コメディー〔文芸〕', value: '307' }, + { label: 'VRゲーム〔SF〕', value: '401' }, + { label: '宇宙〔SF〕', value: '402' }, + { label: '空想科学〔SF〕', value: '403' }, + { label: 'パニック〔SF〕', value: '404' }, + { label: '童話〔その他〕', value: '9901' }, + { label: '詩〔その他〕', value: '9902' }, + { label: 'エッセイ〔その他〕', value: '9903' }, + { label: 'その他〔その他〕', value: '9999' }, + ], + value: '', + }, + modifier: { + type: FilterTypes.Picker, + label: 'Modifier', + options: [ + { label: 'すべて', value: 'total' }, + { label: '連載中', value: 'r' }, + { label: '完結済', value: 'er' }, + { label: '短編', value: 't' }, + ], + value: 'total', + }, + } satisfies Filters; +} + +export default new Syosetu(); diff --git a/plugins/japanese/kakuyomu.ts b/plugins/japanese/kakuyomu.ts new file mode 100644 index 000000000..8f55a0e88 --- /dev/null +++ b/plugins/japanese/kakuyomu.ts @@ -0,0 +1,277 @@ +import { fetchText } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class KakuyomuPlugin implements Plugin.PluginBase { + id = 'kakuyomu'; + name = 'kakuyomu'; + icon = 'src/jp/kakuyomu/icon.png'; + site = 'https://kakuyomu.jp'; + version = '1.0.0'; + filters = { + genre: { + type: FilterTypes.Picker, + label: 'Genre', + options: [ + { label: '総合', value: 'all' }, + { label: '異世界ファンタジー', value: 'fantasy' }, + { label: '現代ファンタジー', value: 'action' }, + { label: 'SF', value: 'sf' }, + { label: '恋愛', value: 'love_story' }, + { label: 'ラブコメ', value: 'romance' }, + { label: '現代ドラマ', value: 'drama' }, + { label: 'ホラー', value: 'horror' }, + { label: 'ミステリー', value: 'mystery' }, + { label: 'エッセイ・ノンフィクション', value: 'nonfiction' }, + { label: '歴史・時代・伝奇', value: 'history' }, + { label: '創作論・評論', value: 'criticism' }, + { label: '詩・童話・その他', value: 'others' }, + ], + value: 'all', + }, + period: { + type: FilterTypes.Picker, + label: 'Period', + options: [ + { label: '累計', value: 'entire' }, + { label: '日間', value: 'daily' }, + { label: '週間', value: 'weekly' }, + { label: '月間', value: 'monthly' }, + { label: '年間', value: 'yearly' }, + ], + value: 'entire', + }, + } satisfies Filters; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const url = new URL( + `/rankings/${filters.genre.value}/${filters.period.value}`, + this.site, + ); + if (pageNo > 1) { + url.searchParams.set('page', pageNo.toString()); + } + const html = await fetchText(url.toString()); + const $ = loadCheerio(html); + const novels: Plugin.NovelItem[] = []; + + $('.widget-media-genresWorkList-right > .widget-work').each((_, elem) => { + const anchor = $(elem).find('a.widget-workCard-titleLabel'); + const path = anchor.attr('href'); + if (!path) return; + const name = anchor.text(); + novels.push({ + name, + path, + cover: defaultCover, + }); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = new URL(novelPath, this.site); + const html = await fetchText(url.toString()); + const $ = loadCheerio(html); + + const json = JSON.parse( + $('script#__NEXT_DATA__[type="application/json"]').html() || '{}', + ); + const apolloState = json?.props?.pageProps?.__APOLLO_STATE__ ?? {}; + + const work = Object.values(apolloState).find( + v => + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'Work' && + 'id' in v && + v.id === novelPath.replace('/works/', ''), + ) as Work; + + const author = Object.values(apolloState).find( + v => + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'UserAccount' && + 'id' in v && + v.id === work.author.__ref.replace('UserAccount:', ''), + ) as UserAccount; + + const chapters = Object.values(apolloState).filter(v => { + if ( + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'Chapter' + ) { + return true; + } + }) as Chapter[]; + + const tableOfContentsChapter = Object.values(apolloState).filter(v => { + if ( + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'TableOfContentsChapter' + ) { + return true; + } + }) as TableOfContentsChapter[]; + + const episodes = Object.values(apolloState).filter(v => { + if ( + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'Episode' + ) { + return true; + } + }) as Episode[]; + + const joinChapters: { + chapter: Chapter | undefined; + episode: Episode; + }[] = episodes.map(v => { + const chapter = tableOfContentsChapter.find(c => + c.episodeUnions.some(e => e.__ref === `Episode:${v.id}`), + )?.chapter; + return { + chapter: chapters.find( + c => c.id === chapter?.__ref.replace('Chapter:', ''), + ), + episode: v, + }; + }); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: work.title, + cover: work.adminCoverImageUrl ?? defaultCover, + genres: (work?.tagLabels ?? []).join(','), + author: author?.activityName, + status: + work.serialStatus === 'COMPLETED' + ? NovelStatus.Completed + : NovelStatus.Ongoing, + summary: work.introduction, + chapters: joinChapters.map(v => { + return { + name: v.chapter?.title + ? `${v.chapter?.title} - ${v.episode?.title}` + : v.episode?.title ?? '', + path: `${novelPath}/episodes/${v.episode?.id}`, + releaseTime: new Date(v.episode?.publishedAt ?? 0).toISOString(), + }; + }), + }; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = new URL(chapterPath, this.site); + const html = await fetchText(url.toString()); + const $ = loadCheerio(html); + const chapterTitle = $('.chapterTitle').text() ?? ''; + const episodeTitle = $('.widget-episodeTitle').html() ?? ''; + const episodeBody = $('.widget-episodeBody').html() ?? ''; + const chapterText = ` + <div> + ${chapterTitle ? `<h1>${chapterTitle}</h1>` : ''} + <h2>${episodeTitle}</h2> + </div> + <p><br><br></p> + ${episodeBody}`; + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = new URL('/search', this.site); + url.searchParams.set('q', searchTerm); + if (pageNo > 1) { + url.searchParams.set('page', pageNo.toString()); + } + const html = await fetchText(url.toString()); + const $ = loadCheerio(html); + + const json = JSON.parse( + $('script#__NEXT_DATA__[type="application/json"]').html() || '{}', + ); + const works = Object.values( + json?.props?.pageProps?.__APOLLO_STATE__ ?? {}, + ).filter(v => { + if ( + typeof v === 'object' && + v !== null && + '__typename' in v && + v.__typename === 'Work' + ) { + return true; + } + }) as Work[]; + + const novels: Plugin.NovelItem[] = works.map(v => ({ + name: v.title ?? '', + path: `/works/${v.id}`, + cover: v.adminCoverImageUrl ?? defaultCover, + })); + + return novels; + } +} + +export default new KakuyomuPlugin(); + +type Work = { + id: string; + title: string; + serialStatus: string; + tagLabels: string[]; + introduction: string; + adminCoverImageUrl?: string; + author: { + __ref: string; + }; +}; + +type UserAccount = { + activityName: string; +}; + +type Chapter = { + id: string; + title: string; +}; + +type Episode = { + title: string; + id: string; + publishedAt: string; +}; + +type TableOfContentsChapter = { + chapter: { + __ref: string; + }; + episodeUnions: { + __ref: string; + }[]; +}; diff --git a/plugins/korean/Agitoon.ts b/plugins/korean/Agitoon.ts new file mode 100644 index 000000000..a4d1bb5aa --- /dev/null +++ b/plugins/korean/Agitoon.ts @@ -0,0 +1,216 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; + +class Agitoon implements Plugin.PluginBase { + id = 'agit.xyz'; + name = 'Agitoon'; + icon = 'src/kr/agitoon/icon.png'; + site = 'https://agit664.xyz'; + version = '3.1.0'; + static url: string | undefined; + + async checkUrl() { + if (!Agitoon.url) { + const res = await fetchApi(this.site); + if (!res.ok) Agitoon.url = this.site; + else Agitoon.url = res.url.replace(/\/$/, ''); + } + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + await this.checkUrl(); + const res = await fetchApi(Agitoon.url + '/novel/index.update.php', { + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + method: 'POST', + body: new URLSearchParams({ + mode: 'get_data_novel_list_p', + novel_menu: showLatestNovels ? '1' : '3', + np_day: new Date().getDay().toString(), + np_rank: '1', + np_distributor: '0', + np_genre: '00', + np_order: '1', + np_genre_ex_1: '00', + np_genre_ex_2: '00', + list_limit: (20 * (pageNo - 1)).toString(), + is_query_first: (pageNo == 1).toString(), + }).toString(), + }); + if (!res.ok) { + throw new Error( + `Failed to get popular novels: (${res.status}: ${res.statusText}) (try to open in webview)`, + ); + } + const resJson = (await res.json()) as response; + const novels: Plugin.NovelItem[] = []; + + resJson?.list?.forEach(novel => + novels.push({ + name: novel.wr_subject, + cover: Agitoon.url + novel.np_dir + '/thumbnail/' + novel.np_thumbnail, + path: novel.wr_id, + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + await this.checkUrl(); + const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(result); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h5.pt-2').text(), + cover: loadedCheerio('div.col-5.pr-0.pl-0 img').attr('src'), + summary: loadedCheerio('.pt-1.mt-1.pb-1.mb-1').text(), + }; + + novel.author = loadedCheerio('.post-item-list-cate-v') + .first() + .text() + .split(' : ')[1]; + + const genres = loadedCheerio('.col-7 > .post-item-list-cate > span') + .map((index, element) => loadedCheerio(element).text().trim()) + .get(); + + if (genres.length) { + novel.genres = genres.join(', '); + } + + const chapters: Plugin.ChapterItem[] = []; + const res = await fetchApi(Agitoon.url + '/novel/list.update.php', { + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + method: 'POST', + body: new URLSearchParams({ + mode: 'get_data_novel_list_c', + wr_id_p: novelPath, + page_no: '1', + cnt_list: '10000', + order_type: 'Asc', + }).toString(), + }); + + const resJson = (await res.json()) as responseBook; + resJson?.list?.forEach(chapter => + chapters.push({ + name: chapter.wr_subject, + path: chapter.wr_id + '/2', + releaseTime: chapter.wr_datetime, + }), + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + await this.checkUrl(); + const result = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); + + const loadedCheerio = parseHTML(result); + let content = loadedCheerio('#id_wr_content').html() || ''; + + // gets rid of the popup thingy + content = content + .replace('팝업메뉴는 빈공간을 더치하거나 스크룰시 사라집니다', '') + .trim(); + + return content; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + await this.checkUrl(); + const rawResults = await fetchApi(Agitoon.url + '/novel/search.php', { + headers: { + 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + method: 'POST', + body: new URLSearchParams({ + mode: 'get_data_novel_list_p_sch', + search_novel: searchTerm, + list_limit: '0', + }).toString(), + }); + const resJson = (await rawResults.json()) as response; + const novels: Plugin.NovelItem[] = []; + + resJson?.list?.forEach(novel => + novels.push({ + name: novel.wr_subject, + cover: + Agitoon.url + '/' + novel.np_dir + '/thumbnail/' + novel.np_thumbnail, + path: novel.wr_id, + }), + ); + + return novels; + } + + resolveUrl(path: string, isNovel?: boolean) { + if (!Agitoon.url) + fetchApi(this.site).then( + res => (Agitoon.url = res.url.replace(/\/$/, '')), + ); + return ( + (Agitoon.url ? Agitoon.url : this.site) + + (isNovel ? '/novel/list/' : '/novel/view/') + + path + ); + } +} + +export default new Agitoon(); + +type response = { + list_limit: number; + list?: ListEntity[] | null; + list_count: number; +}; +type ListEntity = { + wr_id: string; + wr_subject: string; + np_dir: string; + np_type_02: string; + np_thumbnail: string; + np_author: string; + wr_subject2: string; + wr_datetime: string; + np_distributor: string; + np_genre: string; + np_country: string; + np_age: string; + is_scrap: number; +}; + +type responseBook = { + list?: ListEntity2[] | null; + download_time: string; +}; +type ListEntity2 = { + wr_id: string; + wr_subject: string; + wr_datetime: string; + url_view1: string; + novel_c_style1: string; + novel_c_str1: string; + data_novel_c_view: string; +}; diff --git a/plugins/multi/komga.ts b/plugins/multi/komga.ts new file mode 100644 index 000000000..e876050ef --- /dev/null +++ b/plugins/multi/komga.ts @@ -0,0 +1,313 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +import { storage } from '@libs/storage'; + +type Author = { + name: string; + role: string; +}; + +type TocItem = { + title?: string; + href?: string; + children?: TocItem[]; +}; + +type SeriesResponse = { + id: string; + name: string; + metadata: { + status: string; + genres: string[]; + }; + booksMetadata: { + authors: Author[]; + summary: string; + }; +}; + +type BookResponse = { + id: string; + metadata: { + title: string; + }; +}; + +type BookManifest = { + toc: TocItem[]; + readingOrder: { href: string }[]; +}; + +class KomgaPlugin implements Plugin.PluginBase { + id = 'komga'; + name = 'Komga'; + icon = 'src/multi/komga/icon.png'; + version = '1.0.2'; + + site = storage.get('url') as string; + email = storage.get('email'); + password = storage.get('password'); + + async makeRequest(url: string): Promise<string> { + return await fetchApi(url, { + headers: { + Accept: 'application/json, text/plain, */*', + 'Content-Type': 'application/json;charset=utf-8', + 'Authorization': `Basic ${this.btoa(this.email + ':' + this.password)}`, + }, + Referer: this.site, + }).then(res => res.text()); + } + + btoa(input = '') { + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + const str = input; + let output = ''; + + for ( + let block = 0, charCode, i = 0, map = chars; + str.charAt(i | 0) || ((map = '='), i % 1); + output += map.charAt(63 & (block >> (8 - (i % 1) * 8))) + ) { + charCode = str.charCodeAt((i += 3 / 4)); + + if (charCode > 0xff) { + throw new Error( + "'btoa' failed: The string to be encoded contains characters outside of the Latin1 range.", + ); + } + + block = (block << 8) | charCode; + } + + return output; + } + + flattenArray(arr: TocItem[]): TocItem[] { + return arr.reduce((acc: TocItem[], obj: TocItem) => { + const { children, ...rest } = obj; + acc.push(rest); + + if (children) { + acc.push(...this.flattenArray(children)); + } + + return acc; + }, []); + } + + async getSeries(url: string): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + + const response = await this.makeRequest(url); + + const series: SeriesResponse[] = JSON.parse(response).content; + + for (const s of series) { + novels.push({ + name: s.name, + path: 'api/v1/series/' + s.id, + cover: this.site + `api/v1/series/${s.id}/thumbnail`, + }); + } + + return novels; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const read_status = filters?.read_status.value + ? '&read_status=' + filters?.read_status.value + : ''; + const status = filters?.status.value + ? '&status=' + filters?.status.value + : ''; + const sort = showLatestNovels ? 'lastModified,desc' : 'name,asc'; + + const url = `${this.site}api/v1/series?page=${pageNo - 1}${read_status}${status}&sort=${sort}`; + + return await this.getSeries(url); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: 'Untitled', + }; + + const url = this.site + novelPath; + + const response = await this.makeRequest(url); + + const series: SeriesResponse = JSON.parse(response); + + novel.name = series.name; + novel.author = series.booksMetadata.authors + .filter((author: Author) => author.role === 'writer') + .reduce( + (accumulated: string, current: Author) => + accumulated + (accumulated !== '' ? ', ' : '') + current.name, + '', + ); + novel.cover = this.site + `api/v1/series/${series.id}/thumbnail`; + novel.genres = series.metadata.genres.join(', '); + + switch (series.metadata.status) { + case 'ENDED': + novel.status = NovelStatus.Completed; + break; + case 'ONGOING': + novel.status = NovelStatus.Ongoing; + break; + case 'ABANDONED': + novel.status = NovelStatus.Cancelled; + break; + case 'HIATUS': + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + } + + novel.summary = series.booksMetadata.summary; + + const chapters: Plugin.ChapterItem[] = []; + + const booksResponse = await this.makeRequest( + this.site + `api/v1/series/${series.id}/books?unpaged=true`, + ); + + const booksData: BookResponse[] = JSON.parse(booksResponse).content; + + for (const book of booksData) { + const bookManifestResponse = await this.makeRequest( + this.site + `opds/v2/books/${book.id}/manifest`, + ); + + const bookManifest: BookManifest = JSON.parse(bookManifestResponse); + + const toc = this.flattenArray(bookManifest.toc); + + let i = 1; + for (const page of bookManifest.readingOrder) { + const tocItem = toc.find( + (v: TocItem) => v.href?.split('#')[0] === page.href, + ); + const title = tocItem ? tocItem.title : null; + chapters.push({ + name: `${i}/${bookManifest.readingOrder.length} - ${book.metadata.title}${title ? ' - ' + title : ''}`, + path: 'opds/v2' + page.href?.split('opds/v2').pop(), + }); + i++; + } + } + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const chapterText = await this.makeRequest(this.site + chapterPath); + return this.addUrlToImageHref( + chapterText, + this.site + chapterPath.split('/').slice(0, -1).join('/') + '/', + ); + } + + // Convert images to <img> tag and correct url + addUrlToImageHref(htmlString: string, baseUrl: string): string { + const $ = parseHTML(htmlString, { xmlMode: true }); + + // Convert SVG <image> elements to <img> and add baseUrl if necessary + $('svg image').each((_, image) => { + const href = $(image).attr('href') || $(image).attr('xlink:href'); + const width = $(image).attr('width'); + const height = $(image).attr('height'); + + if (href) { + const img = $('<img />').attr({ + src: new URL(href, baseUrl).href, + width: width || '', + height: height || '', + }); + $(image).closest('svg').replaceWith(img); + } + }); + + // Update <img> elements to include the base URL if their src is relative + $('img').each((_, img) => { + const src = $(img).attr('src'); + if (src && !src.startsWith('http')) { + $(img).attr('src', `${baseUrl}${src}`); + } + }); + + // Replace <a> tags with the text inside so its not blue + $('a').each((_, a) => { + $(a).replaceWith($(a).text()); + }); + + return $.xml(); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}api/v1/series?search=${searchTerm}&page=${pageNo - 1}`; + + return await this.getSeries(url); + } + + filters = { + status: { + value: '', + label: 'Status', + options: [ + { label: 'All', value: '' }, + { label: 'Completed', value: NovelStatus.Completed }, + { label: 'Ongoing', value: NovelStatus.Ongoing }, + { label: 'Cancelled', value: NovelStatus.Cancelled }, + { label: 'OnHiatus', value: NovelStatus.OnHiatus }, + ], + type: FilterTypes.Picker, + }, + read_status: { + value: '', + label: 'Read status', + options: [ + { label: 'All', value: '' }, + { label: 'Unread', value: 'UNREAD' }, + { label: 'Read', value: 'READ' }, + { label: 'In progress', value: 'IN_PROGRESS' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; + + pluginSettings = { + email: { + value: '', + label: 'Email', + type: 'Text', + }, + password: { + value: '', + label: 'Password', + }, + url: { + value: '', + label: 'URL', + }, + }; +} + +export default new KomgaPlugin(); diff --git a/plugins/multisrc/fictioneer/custom/cherrymistcafe/chapterTransform.js b/plugins/multisrc/fictioneer/custom/cherrymistcafe/chapterTransform.js new file mode 100644 index 000000000..0f309828d --- /dev/null +++ b/plugins/multisrc/fictioneer/custom/cherrymistcafe/chapterTransform.js @@ -0,0 +1,46 @@ + const ghostScript = loadedCheerio('script[id*=ghost]'); + const contentHost = loadedCheerio('#cherry-content-host'); + + if (ghostScript.length && contentHost.length) { + const poly = ghostScript.attr('data-poly'); + // data-poly attr provide id + // encoded text is stored in attr data-{id}-{number} + // create full string of all the data-poly-nums + const encoded = Array.from( + { length: +ghostScript.attr('data-total') || 0 }, + (_, i) => ghostScript.attr(`data-${poly}-${i}`) || '', + ).join(''); + + // technically copypasta from source + // var c = s.charCodeAt(i); + // if(c>=65 && c<=90) + // o+=String.fromCharCode((c-65+13)%26+65); + // else if(c>=97&&c<=122) + // o+=String.fromCharCode((c-97+13)%26+97); + // else + // o+=s.charAt(i); + if (encoded) { + const rot13 = (str) => { + return str.replace(/[a-zA-Z]/g, (char) => { + const base = char <= 'Z' ? 65 : 97; + const shift = ((char.charCodeAt(0) - base + 13) % 26) + base; + return String.fromCharCode(shift); + }); + }; + contentHost.replaceWith(decodeURIComponent(atob(rot13(encoded)))); + } + } + + loadedCheerio('script, ruby').remove(); + + loadedCheerio('section#chapter-content p [data-fcnc-rev="1"]').each((_, el) => { + const text = loadedCheerio(el).text().trim(); + if (text) loadedCheerio(el).replaceWith([...text].reverse().join('')); + }); + + return ( + loadedCheerio('section#chapter-content > div') + .html() + ?.replace(/\u00A0/g, ' ') + ?.replace(/[\u2060\u00AD\u202F\u2007\u200B]/g, '') || '' + ); diff --git a/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js b/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js new file mode 100644 index 000000000..ef3cddbe6 --- /dev/null +++ b/plugins/multisrc/fictioneer/custom/lillyonthevalley/chapterTransform.js @@ -0,0 +1,36 @@ + const scriptContent = loadedCheerio('script') + .toArray() + .map(script => loadedCheerio(script).html()) + .find(content => content && content.includes('var gib =')); + + if (scriptContent) { + const gibMatch = scriptContent.match(/var gib = (\[.*?\])/); + if (gibMatch) { + const gibArray = eval(gibMatch[1]); + gibArray.forEach(cssClass => { + loadedCheerio(`.${cssClass}`).remove(); + }); + } + } + + loadedCheerio('ruby').remove(); + + loadedCheerio('section#chapter-content p *').each((_, el) => { + if (loadedCheerio(el).attr('data-fcnc-rev') !== '1') return; + const textContent = loadedCheerio(el).text().trim(); + if (textContent) { + loadedCheerio(el).replaceWith( + Array.from(textContent).reverse().join(''), + ); + } + }); + + return ( + loadedCheerio('section#chapter-content > div') + .html() + ?.normalize() + .replace(/\u00A0/g, ' ') + .replace(/\u2060/g, '') + .replace(/­/g, '') // ­ + .replace(/[\u202F\u2007\u200B]/g, '') || '' + ); diff --git a/plugins/multisrc/fictioneer/generator.js b/plugins/multisrc/fictioneer/generator.js new file mode 100644 index 000000000..452a878e6 --- /dev/null +++ b/plugins/multisrc/fictioneer/generator.js @@ -0,0 +1,41 @@ +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +import list from './sources.json' with { type: 'json' }; + +export const generateAll = function () { + return list.map(source => { + console.log( + `[fictioneer] Generating: ${source.id}${' '.repeat(20 - source.id.length)}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const readNovelFullTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const chapterTransformJsOrPath = source.options?.customJs?.chapterTransform; + const chapterTransformPath = chapterTransformJsOrPath + ? join(folder, chapterTransformJsOrPath) + : ''; + const chapterTransformJs = existsSync(chapterTransformPath) + ? readFileSync(chapterTransformPath, { encoding: 'utf-8' }) + : chapterTransformJsOrPath; + + const pluginScript = ` +${readNovelFullTemplate.replace('// chapterTransformJs HERE', chapterTransformJs || '')} +const plugin = new FictioneerPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + return { + lang: source.options?.lang || 'English', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/fictioneer/sources.json b/plugins/multisrc/fictioneer/sources.json new file mode 100644 index 000000000..06cc73009 --- /dev/null +++ b/plugins/multisrc/fictioneer/sources.json @@ -0,0 +1,69 @@ +[ + { + "id": "daoistquest", + "sourceSite": "https://daoist.quest", + "sourceName": "Daoist Quest", + "options": { + "browsePage": "collection/novels", + "versionIncrements": 1 + } + }, + { + "id": "novelib", + "sourceSite": "https://novelib.com", + "sourceName": "NovelLib", + "options": { + "browsePage": "browse" + } + }, + { + "id": "lilyonthevalley", + "sourceSite": "https://lilyonthevalley.com", + "sourceName": "Lily on the Valley", + "options": { + "customJs": { + "chapterTransform": "custom/lillyonthevalley/chapterTransform.js" + }, + "versionIncrements": 1, + "browsePage": "stories" + } + }, + { + "id": "penguinsquad", + "sourceSite": "https://penguin-squad.com", + "sourceName": "Penguin Squad", + "options": { + "browsePage": "novels" + } + }, + { + "id": "prizmatranslation", + "sourceSite": "https://prizmatranslation.com", + "sourceName": "Prizma", + "options": { + "browsePage": "home/all-novels" + } + }, + { + "id": "dearestrosalie", + "sourceSite": "https://dearestrosalie.com", + "sourceName": "Dearest Rosalie", + "options": { + "browsePage": "stories", + "down": true, + "downSince": 1768289212907 + } + }, + { + "id": "cherrymistcafe", + "sourceSite": "https://cherrymist.cafe/", + "sourceName": "Cherry Mist Cafe", + "options": { + "customJs": { + "chapterTransform": "custom/cherrymistcafe/chapterTransform.js" + }, + "versionIncrements": 1, + "browsePage": "stories" + } + } +] diff --git a/plugins/multisrc/fictioneer/template.ts b/plugins/multisrc/fictioneer/template.ts new file mode 100644 index 000000000..268fdf7db --- /dev/null +++ b/plugins/multisrc/fictioneer/template.ts @@ -0,0 +1,171 @@ +import { CheerioAPI, load as loadCheerio } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters } from '@libs/filterInputs'; + +type FictioneerOptions = { + browsePage: string; + lang?: string; + versionIncrements?: number; +}; + +export type FictioneerMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options: FictioneerOptions; +}; + +export class FictioneerPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options: FictioneerOptions; + filters: Filters | undefined = undefined; + + constructor(metadata: FictioneerMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/fictioneer/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `1.1.${0 + versionIncrements}`; + this.options = metadata.options; + } + + private parseNovels( + loadedCheerio: CheerioAPI, + selector: string, + ): Plugin.NovelItem[] { + return loadedCheerio(selector) + .map((i, el) => { + const element = loadedCheerio(el); + const novelName = element.find('h3 > a').text(); + const novelCover = element.find('a.cell-img:has(img)').attr('href'); + const novelUrl = element.find('h3 > a').attr('href'); + + if (!novelUrl) return; + + return { + name: novelName, + cover: novelCover, + path: new URL(novelUrl, this.site).pathname.substring(1), + }; + }) + .toArray(); + } + + async popularNovels( + pageNo: number, + // { + // showLatestNovels, + // filters, + // }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const req = await fetchApi( + this.site + + '/' + + this.options.browsePage + + '/' + + (pageNo === 1 ? '' : 'page/' + pageNo + '/'), + ); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + return this.parseNovels( + loadedCheerio, + '#featured-list > li > div > div, #list-of-stories > li > div > div', + ); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const req = await fetchApi(this.site + '/' + novelPath + '/'); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.story__identity-title').text(), + }; + + // novel.artist = ''; + novel.author = loadedCheerio('div.story__identity-meta') + .text() + .split('|')[0] + .replace('Author: ', '') + .replace('by ', '') + .trim(); + novel.cover = loadedCheerio('figure.story__thumbnail > a').attr('href'); + novel.genres = loadedCheerio('div.tag-group > a, section.tag-group > a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + loadedCheerio('section.story__summary .related-stories-block').remove(); + novel.summary = loadedCheerio('section.story__summary').text(); + + novel.chapters = loadedCheerio('li.chapter-group__list-item._publish') + .filter((i, el) => !el.attribs['class'].includes('_password')) + .filter( + (i, el) => + !loadedCheerio(el) + .find('i') + .first()! + .attr('class')! + .includes('fa-lock'), + ) + .map((i, el) => { + const chapterName = loadedCheerio(el).find('a').text(); + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + + if (!chapterUrl) return; + return { + name: chapterName, + path: new URL(chapterUrl, this.site).pathname.substring(1), + }; + }) + .toArray(); + + const status = loadedCheerio('span.story__status').text().trim(); + if (status === 'Ongoing') novel.status = NovelStatus.Ongoing; + if (status === 'Completed') novel.status = NovelStatus.Completed; + if (status === 'Cancelled') novel.status = NovelStatus.Cancelled; + if (status === 'Hiatus') novel.status = NovelStatus.OnHiatus; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const req = await fetchApi(this.site + '/' + chapterPath + '/'); + const body = await req.text(); + + const loadedCheerio = loadCheerio(body); + + // chapterTransformJs HERE + + return loadedCheerio('section#chapter-content > div').html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const req = await fetchApi( + this.site + + `/${pageNo === 1 ? '' : 'page/' + pageNo + '/'}?s=${encodeURIComponent(searchTerm)}&post_type=fcn_story`, + ); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + return this.parseNovels( + loadedCheerio, + '#search-result-list > li > div > div', + ); + } + + // resolveUrl = (path: string, isNovel?: boolean) => + // this.site + '/' + path + '/'; +} diff --git a/plugins/multisrc/generate-multisrc-plugins.js b/plugins/multisrc/generate-multisrc-plugins.js new file mode 100644 index 000000000..17cd119b0 --- /dev/null +++ b/plugins/multisrc/generate-multisrc-plugins.js @@ -0,0 +1,59 @@ +import path from 'path'; +import fs from 'fs'; + +// type GeneratedScript = { +// lang: string; +// filename: string; +// pluginScript: string; +// }; + +// export type ScrpitGeneratorFunction = () => GeneratedScript[]; + +const isScriptGenerator = s => { + return !!s && typeof s === 'function'; +}; + +const generate = async name => { + try { + const generateAll = (await import(`./${name}/generator.js`)).generateAll; + if (!isScriptGenerator(generateAll)) return false; + const sources = generateAll(); + for (let source of sources) { + const { lang, filename, pluginScript } = source; + if (!lang || !filename || !pluginScript) { + console.warn(name, ': lang, filename, pluginScript are required!'); + continue; + } + const pluginsDir = './plugins'; + const filePath = path.join( + pluginsDir, + lang.toLowerCase(), + filename.replace(/[\s-.]+/g, '') + `[${name}].ts`, + ); + fs.writeFileSync(filePath, pluginScript, { encoding: 'utf-8' }); + } + return true; + } catch (e) { + console.log(`${name} is broken! ${e}\n`); + return false; + } +}; + +const MULTISRC_DIR = './plugins/multisrc'; + +const run = async () => { + const sources = fs + .readdirSync(MULTISRC_DIR) + .filter( + name => + fs.lstatSync(path.join(MULTISRC_DIR, name)).isDirectory() && + !name.endsWith('.broken'), + ); + + for (let name of sources) { + const success = await generate(name); + if (success) console.log(`[${name}] OK`); + } +}; + +run(); diff --git a/plugins/multisrc/hotnovelpub/filter_refresh.ts b/plugins/multisrc/hotnovelpub/filter_refresh.ts new file mode 100644 index 000000000..fdcde0e2e --- /dev/null +++ b/plugins/multisrc/hotnovelpub/filter_refresh.ts @@ -0,0 +1,84 @@ +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import list from './sources.json' with { type: 'json' }; +import { HotNovelPubMetadata } from './template'; +import { Filters } from '@libs/filterInputs'; + +async function getFilters(sources: HotNovelPubMetadata) { + const filters: Filters = { + sort: { + type: 'Picker', + label: 'Order', + value: 'hot', + options: [], + }, + category: { + type: 'Picker', + label: 'category', + value: '', + options: [{ label: 'NONE', value: '' }], + }, + }; + const body = await fetch(sources.sourceSite).then(res => res.text()); + const $: cheerio.CheerioAPI = cheerio.load(body); + $('.new-update').remove(); + + $('section > div').each((i, el) => { + const id = $(el).find('a[class="see-all"]').attr('href'); + if (id) { + filters.sort.options.push({ + label: $(el).find('[class="section-title"]').text().trim(), + value: id.split('/').pop(), + }); + } + }); + + filters.category.label = $('.category-title').text().trim(); + const apiSite = sources.sourceSite.replace('://', '://api.'); + const jsonRaw = await fetch(apiSite + '/categories', { + headers: { + lang: sources.options?.lang || 'en', + }, + }); + const json = (await jsonRaw.json()) as response; + + if (json.data?.length) { + json.data + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach(category => + filters.category.options.push({ + label: category.name, + value: category.slug, + }), + ); + } + return filters; +} + +type response = { + status: number; + message: string; + data?: DataEntity[]; +}; +type DataEntity = { + id: number; + name: string; + slug: string; +}; + +async function start() { + const result = []; + for (const sources of list) { + console.log('updating the filters in', sources.sourceName); + const NewFilters = await getFilters(sources as HotNovelPubMetadata); + sources.filters = NewFilters; + result.push(sources); + } + fs.writeFileSync( + path.join(__dirname, 'sources.json'), + JSON.stringify(result, null, 2), + ); +} + +start(); diff --git a/plugins/multisrc/hotnovelpub/generator.js b/plugins/multisrc/hotnovelpub/generator.js new file mode 100644 index 000000000..7daca0601 --- /dev/null +++ b/plugins/multisrc/hotnovelpub/generator.js @@ -0,0 +1,60 @@ +import list from './sources.json' with { type: 'json' }; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); +const FilterTypes = { + TextInput: 'Text', + Picker: 'Picker', + CheckboxGroup: 'Checkbox', + Switch: 'Switch', + ExcludableCheckboxGroup: 'XCheckbox', +}; + +const lang = { + en: 'english', + ru: 'russian', + es: 'spanish', + pt: 'portuguese', + th: 'turkish', +}; + +export const generateAll = function () { + return list + .map(p => { + const filters = {}; + for (const k in p.filters) { + const f = p.filters[k]; + if (f) { + filters[k] = { + ...f, + type: FilterTypes.Picker, + }; + } + } + return { ...p, filters }; + }) + .map(metadata => { + console.log(`[hotnovelpub]: Generating`, metadata.id); + return generator(metadata); + }); +}; + +const generator = function generator(metadata) { + const HotNovelPubTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` + ${HotNovelPubTemplate} +const plugin = new HotNovelPubPlugin(${JSON.stringify(metadata)}); +export default plugin; + `.trim(); + + return { + lang: lang[metadata?.options?.lang || 'en'] || 'english', + filename: metadata.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/hotnovelpub/sources.json b/plugins/multisrc/hotnovelpub/sources.json new file mode 100644 index 000000000..1e9bb6863 --- /dev/null +++ b/plugins/multisrc/hotnovelpub/sources.json @@ -0,0 +1,393 @@ +[ + { + "id": "hotnovelpub", + "sourceSite": "https://hotnovelpub.com", + "sourceName": "HotNovelPub", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Hot story", + "value": "hot" + }, + { + "label": "New story", + "value": "new" + }, + { + "label": "Full story", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Category", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Adult Romance", + "value": "adult-romance" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "LGBTQ+", + "value": "lgbtq" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "New Adult", + "value": "new-adult" + }, + { + "label": "Paranormal", + "value": "paranormal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Steamy Stories", + "value": "steamy-stories" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Werewolf/Vampire", + "value": "werewolfvampire" + } + ] + } + } + }, + { + "id": "eznovels", + "sourceSite": "https://eznovels.com", + "sourceName": "EzNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Горячая история", + "value": "hot" + }, + { + "label": "Новая история", + "value": "new" + }, + { + "label": "Полный рассказ", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Категория", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Ведьмы", + "value": "vedmy" + }, + { + "label": "Городское фэнтези", + "value": "gorodskoe-fentezi" + }, + { + "label": "Городской роман", + "value": "gorodskoj-roman" + }, + { + "label": "Любовное фэнтези", + "value": "lyubovnoe-fentezi" + }, + { + "label": "Оборотни", + "value": "oborotni" + }, + { + "label": "Сверхъестественное", + "value": "sverhuestestvennoe" + }, + { + "label": "Современный любовный роман", + "value": "sovremennyj-lyubovnyj-roman" + }, + { + "label": "Триллеры", + "value": "trillery" + }, + { + "label": "Фэнтези", + "value": "fentezi" + }, + { + "label": "Эротика", + "value": "erotika" + } + ] + } + }, + "options": { + "lang": "ru" + } + }, + { + "id": "lightnoveldaily", + "sourceSite": "https://lightnoveldaily.com", + "sourceName": "LightNovelDaily", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "Historia caliente", + "value": "hot" + }, + { + "label": "Nuevas cuentos", + "value": "new" + }, + { + "label": "Historia completa", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Categoría", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Acción/Aventura", + "value": "accinaventura" + }, + { + "label": "Fantasía", + "value": "fantasa" + }, + { + "label": "Ficción adolescente", + "value": "ficcin-adolescente" + }, + { + "label": "Hombre lobo/Vampiro", + "value": "hombre-lobovampiro" + }, + { + "label": "LGBTQ+", + "value": "lgbtq" + }, + { + "label": "Mafia", + "value": "mafia" + }, + { + "label": "Nuevo Adulto", + "value": "nuevo-adulto" + }, + { + "label": "Pornografía", + "value": "pornografa" + }, + { + "label": "Romántico", + "value": "romntico" + }, + { + "label": "Urbano", + "value": "urbano" + } + ] + } + }, + "options": { + "lang": "es", + "down": true, + "downSince": 1768289212956 + } + }, + { + "id": "lanovels", + "sourceSite": "https://lanovels.com", + "sourceName": "LaNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "História quente", + "value": "hot" + }, + { + "label": "Nova estória", + "value": "new" + }, + { + "label": "História completa", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "Categoria", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Erótico", + "value": "erotico" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Ficção adolescente", + "value": "ficcao-adolescente" + }, + { + "label": "Ficção científica", + "value": "ficcao-cientifica" + }, + { + "label": "Lobisomens/Vampiros", + "value": "lobisomens-vampiros" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Paranormal", + "value": "paranormal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Suspense/Terror", + "value": "suspense-terror" + } + ] + } + }, + "options": { + "lang": "pt", + "down": true, + "downSince": 1768289212942 + } + }, + { + "id": "thnovels", + "sourceSite": "https://thnovels.com", + "sourceName": "ThNovels", + "filters": { + "sort": { + "type": "Picker", + "label": "Order", + "value": "hot", + "options": [ + { + "label": "เรื่องร้อน", + "value": "hot" + }, + { + "label": "เรื่องใหม่", + "value": "new" + }, + { + "label": "เรื่องเต็ม", + "value": "full" + } + ] + }, + "category": { + "type": "Picker", + "label": "หมวดหมู่", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "ใช้ชีวิต", + "value": "ใช้ชีวิต" + }, + { + "label": "นิยายY", + "value": "นิยายy" + }, + { + "label": "นิยายสำหรับผู้ใหญ่", + "value": "นิยายสำหรับผู้ใหญ่" + }, + { + "label": "ประวัติศาสตร์", + "value": "ประวัติศาสตร์" + }, + { + "label": "แฟนตาซี", + "value": "แฟนตาซี" + }, + { + "label": "มนุษย์หมาป่าแวมไพร์", + "value": "มนุษย์หมาป่าแวมไพร์" + }, + { + "label": "โรแมนซ์", + "value": "โรแมนซ์" + } + ] + } + }, + "options": { + "lang": "th", + "down": true, + "downSince": 1768289212962 + } + } +] diff --git a/plugins/multisrc/hotnovelpub/template.ts b/plugins/multisrc/hotnovelpub/template.ts new file mode 100644 index 000000000..158952883 --- /dev/null +++ b/plugins/multisrc/hotnovelpub/template.ts @@ -0,0 +1,278 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; + +export type HotNovelPubMetadata = { + id: string; + sourceSite: string; + sourceName: string; + filters?: Filters; + options?: HotNovelPubOptions; +}; + +type HotNovelPubOptions = { + lang?: string; +}; + +export class HotNovelPubPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + apiSite: string; + version: string; + filters?: Filters; + lang: string; + + constructor(metadata: HotNovelPubMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/hotnovelpub/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + this.apiSite = metadata.sourceSite.replace('://', '://api.'); + this.version = '1.0.1'; + this.filters = metadata.filters; + this.lang = metadata.options?.lang || 'en'; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.apiSite + '/books/'; + url += showLatestNovels ? 'new' : filters?.sort?.value || 'hot'; + if (filters?.category?.value) { + url = this.apiSite + '/category/' + filters.category.value; + } + + url += '/?page=' + (pageNo - 1) + '&limit=20'; + + const result: responseNovels = await fetchApi(url, { + headers: { + lang: this.lang, + }, + }).then((res: Response) => res.json()); + const novels: Plugin.NovelItem[] = []; + + if (result.status && result.data.books.data?.length) { + result.data.books.data.forEach(novel => + novels.push({ + name: novel.name, + cover: this.site + novel.image, + path: novel.slug, + }), + ); + } + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const json: responseNovel = await fetchApi( + this.apiSite + '/book/' + novelPath, + { + headers: { + lang: this.lang, + }, + }, + ).then((res: Response) => res.json()); + + const novel: Plugin.SourceNovel = { + name: json.data.book.name, + path: novelPath, + cover: this.site + json.data.book.image, + summary: json.data.book.authorize.description, + author: json.data.book.authorize.name, + status: + json.data.book.status === 'updating' + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + if (json.data.tags.tags_name?.length) { + novel.genres = json.data.tags.tags_name.join(','); + } + + if (json.data.chapters?.length) { + const chapters: Plugin.ChapterItem[] = []; + json.data.chapters.forEach((chapter, chapterIndex) => + chapters.push({ + name: chapter.title, + path: chapter.slug, + releaseTime: undefined, + chapterNumber: (chapter.index || chapterIndex) + 1, + }), + ); + + novel.chapters = chapters; + } + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.resolveUrl(chapterPath)).then( + (res: Response) => res.text(), + ); + + let chapterText = + body.match(/<div id="content-item" ([\s\S]*?)<\/div>/)?.[0] || ''; + + if (chapterText) { + const result = await fetchApi( + this.site + '/server/getContent?slug=' + chapterPath, + ); + const json = (await result.json()) as ChapterType; + + if (json.data) { + chapterText += json.data + .map(item => '<p>' + item + '</p>') + .join('') + .replace(/\n/g, '</p><p>') + .replace(/\s/g, ' '); + } + } + return chapterText.replace(/\.copy right hot novel pub/g, ''); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const result: responseSearch = await fetchApi(this.apiSite + '/search', { + headers: { + 'Content-Type': 'application/json;charset=utf-8', + Referer: this.site, + Origin: this.site, + lang: this.lang, + }, + method: 'POST', + body: JSON.stringify({ key_search: searchTerm }), + }).then((res: Response) => res.json()); + const novels: Plugin.NovelItem[] = []; + + if (result.status && result.data.books?.length) { + result.data.books.forEach(novel => + novels.push({ + name: novel.name, + path: novel.slug, + }), + ); + } + + return novels; + } + resolveUrl = (path: string) => this.site + '/' + path; +} + +type responseNovels = { + status: number; + message: string; + data: Data; +}; +type Data = { + category?: Category; + books: Books; +}; +type Category = { + id: number; + name: string; + description: string; + slug: string; + createdAt: string; + updatedAt: string; + countryId: number; +}; +type Books = { + total: number; + pages_count: number; + data?: DataEntity[]; +}; +type DataEntity = { + id: number; + name: string; + view: number; + status: string; + image: string; + slug: string; + categories?: CategoriesEntity[] | null; + source?: null; +}; +type CategoriesEntity = { + id: number; + name: string; + slug?: string; +}; + +type responseNovel = { + status: number; + message: string; + data: Data1; +}; +type Data1 = { + book: Book1; + tags: Tags; + chapters?: ChaptersEntity[]; +}; +type Book1 = { + id: number; + name: string; + description: string; + view: number; + rate: number; + status: string; + image: string; + createdAt: string; + chapterCount: number; + crawlStt: string; + rateCount: number; + slug: string; + categories?: CategoriesEntity[] | null; + authorize: Authorize; + updated_at: string; + source?: null; + idSource?: null; + prize?: null; +}; +type Authorize = { + id: number; + name: string; + description: string; + avatar: string; + slug: string; +}; +type Tags = { + tags_name?: string[] | null; + tags?: CategoriesEntity[] | null; +}; +type ChaptersEntity = { + id: number; + title: string; + slug: string; + index: number; +}; + +type ChapterType = { + type: string; + data?: string[]; +}; + +type responseSearch = { + status: number; + message: string; + data: Data2; +}; +type Data2 = { + books?: BooksEntity[] | null; + authorizes?: AuthorizesEntityOrCategoriesEntity[] | null; + categories?: AuthorizesEntityOrCategoriesEntity[] | null; +}; +type BooksEntity = { + id: number; + name: string; + slug: string; +}; +type AuthorizesEntityOrCategoriesEntity = { + id: number; + name: string; + slug: string; +}; diff --git a/plugins/multisrc/ifreedom/generator.js b/plugins/multisrc/ifreedom/generator.js new file mode 100644 index 000000000..032768446 --- /dev/null +++ b/plugins/multisrc/ifreedom/generator.js @@ -0,0 +1,33 @@ +import list from './sources.json' with { type: 'json' }; +import defaultSettings from './settings.json' with { type: 'json' }; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(metadata => { + metadata.filters = Object.assign(defaultSettings.filters, metadata.filters); + console.log(`[ifreedom]: Generating`, metadata.id); + return generator(metadata); + }); +}; + +const generator = function generator(metadata) { + const IfreedomTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` + ${IfreedomTemplate} +const plugin = new IfreedomPlugin(${JSON.stringify(metadata)}); +export default plugin; + `.trim(); + + return { + lang: 'russian', + filename: metadata.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/ifreedom/settings.json b/plugins/multisrc/ifreedom/settings.json new file mode 100644 index 000000000..c524415c2 --- /dev/null +++ b/plugins/multisrc/ifreedom/settings.json @@ -0,0 +1,260 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Сортировка:", + "options": [ + { + "label": "По дате добавления", + "value": "По дате добавления" + }, + { + "label": "По дате обновления", + "value": "По дате обновления" + }, + { + "label": "По количеству глав", + "value": "По количеству глав" + }, + { + "label": "По названию", + "value": "По названию" + }, + { + "label": "По просмотрам", + "value": "По просмотрам" + }, + { + "label": "По рейтингу", + "value": "По рейтингу" + } + ], + "value": "По рейтингу" + }, + "status": { + "type": "CheckboxGroup", + "label": "Статус:", + "options": [ + { + "label": "Перевод активен", + "value": "Перевод активен" + }, + { + "label": "Перевод приостановлен", + "value": "Перевод приостановлен" + }, + { + "label": "Произведение завершено", + "value": "Произведение завершено" + } + ], + "value": [] + }, + "lang": { + "type": "CheckboxGroup", + "label": "Язык:", + "options": [ + { + "label": "Английский", + "value": "Английский" + }, + { + "label": "Китайский", + "value": "Китайский" + }, + { + "label": "Корейский", + "value": "Корейский" + }, + { + "label": "Японский", + "value": "Японский" + } + ], + "value": [] + }, + "genre": { + "type": "CheckboxGroup", + "label": "Жанры:", + "options": [ + { + "label": "Боевик", + "value": "Боевик" + }, + { + "label": "Боевые Искусства", + "value": "Боевые Искусства" + }, + { + "label": "Вампиры", + "value": "Вампиры" + }, + { + "label": "Виртуальный Мир", + "value": "Виртуальный Мир" + }, + { + "label": "Гарем", + "value": "Гарем" + }, + { + "label": "Героическое фэнтези", + "value": "Героическое фэнтези" + }, + { + "label": "Детектив", + "value": "Детектив" + }, + { + "label": "Дзёсэй", + "value": "Дзёсэй" + }, + { + "label": "Драма", + "value": "Драма" + }, + { + "label": "Игра", + "value": "Игра" + }, + { + "label": "История", + "value": "История" + }, + { + "label": "Киберпанк", + "value": "Киберпанк" + }, + { + "label": "Комедия", + "value": "Комедия" + }, + { + "label": "ЛитРПГ", + "value": "ЛитРПГ" + }, + { + "label": "Меха", + "value": "Меха" + }, + { + "label": "Милитари", + "value": "Милитари" + }, + { + "label": "Мистика", + "value": "Мистика" + }, + { + "label": "Научная Фантастика", + "value": "Научная Фантастика" + }, + { + "label": "Повседневность", + "value": "Повседневность" + }, + { + "label": "Постапокалипсис", + "value": "Постапокалипсис" + }, + { + "label": "Приключения", + "value": "Приключения" + }, + { + "label": "Психология", + "value": "Психология" + }, + { + "label": "Романтика", + "value": "Романтика" + }, + { + "label": "Сверхъестественное", + "value": "Сверхъестественное" + }, + { + "label": "Сёдзё", + "value": "Сёдзё" + }, + { + "label": "Сёнэн", + "value": "Сёнэн" + }, + { + "label": "Сёнэн-ай", + "value": "Сёнэн-ай" + }, + { + "label": "Спорт", + "value": "Спорт" + }, + { + "label": "Сэйнэн", + "value": "Сэйнэн" + }, + { + "label": "Сюаньхуа", + "value": "Сюаньхуа" + }, + { + "label": "Трагедия", + "value": "Трагедия" + }, + { + "label": "Триллер", + "value": "Триллер" + }, + { + "label": "Ужасы", + "value": "Ужасы" + }, + { + "label": "Фантастика", + "value": "Фантастика" + }, + { + "label": "Фэнтези", + "value": "Фэнтези" + }, + { + "label": "Школьная жизнь", + "value": "Школьная жизнь" + }, + { + "label": "Экшн", + "value": "Экшн" + }, + { + "label": "Эротика", + "value": "Эротика" + }, + { + "label": "Этти", + "value": "Этти" + }, + { + "label": "Яой", + "value": "Яой" + }, + { + "label": "Adult", + "value": "Adult" + }, + { + "label": "Mature", + "value": "Mature" + }, + { + "label": "Xianxia", + "value": "Xianxia" + }, + { + "label": "Xuanhuan", + "value": "Xuanhuan" + } + ], + "value": [] + } + } +} diff --git a/plugins/multisrc/ifreedom/sources.json b/plugins/multisrc/ifreedom/sources.json new file mode 100644 index 000000000..3b24b1e99 --- /dev/null +++ b/plugins/multisrc/ifreedom/sources.json @@ -0,0 +1,14 @@ +[ + { + "id": "ifreedom", + "sourceSite": "https://ifreedom.su", + "sourceName": "Свободный Мир Ранобэ", + "filters": {} + }, + { + "id": "bookhamster", + "sourceSite": "https://bookhamster.ru", + "sourceName": "Bookhamster", + "filters": {} + } +] diff --git a/plugins/multisrc/ifreedom/template.ts b/plugins/multisrc/ifreedom/template.ts new file mode 100644 index 000000000..372224807 --- /dev/null +++ b/plugins/multisrc/ifreedom/template.ts @@ -0,0 +1,400 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters, FilterToValues } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Parser } from 'htmlparser2'; +import dayjs from 'dayjs'; + +export type IfreedomMetadata = { + id: string; + sourceSite: string; + sourceName: string; + filters?: Filters; +}; + +export class IfreedomPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + filters?: Filters; + + constructor(metadata: IfreedomMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/ifreedom/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + this.version = '1.1.1'; + this.filters = metadata.filters; + } + + parseNovels(url: string) { + return fetchApi(url) + .then((res: Response) => res.text()) + .then((html: string) => { + const novels: Plugin.NovelItem[] = []; + let tempNovel = {} as Plugin.NovelItem; + let isInsideNovelCard = false; + const site = this.site; + + const parser = new Parser({ + onopentag(name, attribs) { + const className = attribs['class'] || ''; + if ( + name === 'div' && + (className.includes('one-book-home') || + className.includes('item-book-slide')) + ) { + isInsideNovelCard = true; + } + + if (isInsideNovelCard) { + if (name === 'img') { + tempNovel.cover = attribs['src']; + if (attribs['alt']) tempNovel.name = attribs['alt']; + } + if (name === 'a' && attribs['href']) { + tempNovel.path = attribs['href'].replace(site, ''); + if (attribs['title']) tempNovel.name = attribs['title']; + } + } + }, + onclosetag(name) { + if (name === 'div' && isInsideNovelCard) { + isInsideNovelCard = false; + if (tempNovel.path) novels.push(tempNovel); + tempNovel = {} as Plugin.NovelItem; + } + }, + }); + parser.write(html); + parser.end(); + return novels; + }); + } + + async popularNovels( + page: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = `${this.site}/vse-knigi/?sort=${showLatestNovels ? 'По дате обновления' : filters?.sort?.value || 'По рейтингу'}`; + + Object.entries(filters || {}).forEach(([type, filter]) => { + const { value } = filter as FilterToValues<Filters>[string]; + if (Array.isArray(value) && value.length) { + url += `&${type}[]=${value.join(`&${type}[]=`)}`; + } + }); + + url += `&bpage=${page}`; + return this.parseNovels(url); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const html = await fetchApi(this.site + novelPath).then((res: Response) => + res.text(), + ); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + author: '', + summary: '', + status: NovelStatus.Unknown, + }; + const chapters: Plugin.ChapterItem[] = []; + const genres: string[] = []; + const site = this.site; + + let isReadingName = false; + let isReadingSummary = false; + let isCoverContainer = false; + + let metaContext: 'author' | 'status' | 'genre' | null = null; + let isMetaRow = false; + let isMetaValue = false; + + let isInsideChapterRow = false; + let isReadingChapterName = false; + let isReadingChapterDate = false; + let tempChapter = {} as Plugin.ChapterItem; + + const parser = new Parser({ + onopentag(name, attribs) { + const className = attribs['class'] || ''; + + if (name === 'h1') isReadingName = true; + + if (name === 'div') { + if ( + className.includes('block-book-slide-img') || + className.includes('img-ranobe') + ) { + isCoverContainer = true; + } + if ( + className === 'descr-ranobe' || + (className === 'active' && attribs['data-name'] === 'Описание') + ) { + isReadingSummary = true; + } + } + + if ( + isReadingSummary && + name === 'span' && + className.includes('open-desc') + ) { + const onclick = attribs['onclick']; + if (onclick) { + const match = onclick.match(/innerHTML\s*=\s*'([\s\S]+?)'/); + if (match && match[1]) { + let fullText = match[1]; + fullText = fullText + .replace(/<br>/gi, '\n') + .replace(/<br\s*\/?>/gi, '\n') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/&/g, '&'); + + novel.summary = fullText; + isReadingSummary = false; + } + } + } + + if (name === 'img' && isCoverContainer && !novel.cover) { + novel.cover = attribs['src']; + } + + if (name === 'div') { + if (className.includes('data-ranobe')) { + isMetaRow = true; + metaContext = null; + } + if (className.includes('data-value')) { + isMetaValue = true; + } + + if (className.includes('book-info-list')) { + isMetaRow = true; + metaContext = null; + } + if (className.includes('genreslist')) { + metaContext = 'genre'; + } + } + + if (isMetaRow) { + if (name === 'span') { + if ( + className.includes('dashicons-book') && + !className.includes('book-alt') + ) + metaContext = 'genre'; + else if (className.includes('admin-users')) metaContext = 'author'; + else if (className.includes('megaphone')) metaContext = 'status'; + } + if (name === 'svg') { + if (className.includes('icon-tabler-tag')) metaContext = 'genre'; + else if ( + className.includes('mood-edit') || + className.includes('icon-tabler-user') + ) + metaContext = 'author'; + else if ( + className.includes('chart-infographic') || + className.includes('megaphone') + ) + metaContext = 'status'; + } + } + + if ( + name === 'div' && + (className === 'li-ranobe' || className === 'chapterinfo') + ) { + isInsideChapterRow = true; + } + if (name === 'a' && isInsideChapterRow) { + tempChapter.path = attribs['href'].replace(site, ''); + isReadingChapterName = true; + } + if ( + (name === 'div' || name === 'span') && + (className === 'li-col2-ranobe' || className === 'timechapter') + ) { + isReadingChapterDate = true; + } + }, + ontext(data) { + const text = data.trim(); + if (!text) return; + + if (isReadingName) novel.name = text.replace(/®/g, '').trim(); + if (isReadingSummary && text !== 'Прочесть полностью') { + novel.summary += text + '\n'; + } + + if (metaContext) { + const shouldRead = isMetaValue || (isMetaRow && !isMetaValue); + if (shouldRead) { + if (metaContext === 'author') { + if ( + text !== 'Автор' && + text !== 'Переводчик' && + text !== 'Не указан' && + !text.includes('Просмотров') + ) { + novel.author = text; + } + } else if (metaContext === 'status') { + if (!text.includes('Статус')) novel.status = parseStatus(text); + } else if (metaContext === 'genre') { + if (text !== ',' && text !== 'Жанры') genres.push(text); + } + } + } + + if (isReadingChapterName) tempChapter.name = text; + if (isReadingChapterDate) tempChapter.releaseTime = parseDate(text); + }, + onclosetag(name) { + if (name === 'h1') isReadingName = false; + if (name === 'div') { + if (isReadingSummary) isReadingSummary = false; + if (isCoverContainer) isCoverContainer = false; + if (isMetaValue) isMetaValue = false; + } + + if (name === 'a') isReadingChapterName = false; + if ((name === 'div' || name === 'span') && isReadingChapterDate) { + isReadingChapterDate = false; + if (tempChapter.path) { + chapters.push(tempChapter); + } + tempChapter = {} as Plugin.ChapterItem; + isInsideChapterRow = false; + } + }, + }); + + parser.write(html); + parser.end(); + + novel.genres = genres.join(','); + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then((res: Response) => + res.text(), + ); + + const startTag = + this.id === 'bookhamster' + ? '<div class="entry-content">' + : '<div class="chapter-content">'; + const endTag = + this.id === 'bookhamster' + ? '<!-- .entry-content -->' + : '<div class="chapter-setting">'; + + const chapterStart = body.indexOf(startTag); + if (chapterStart === -1) return ''; + + const chapterEnd = body.indexOf(endTag, chapterStart); + let chapterText = body.slice( + chapterStart, + chapterEnd !== -1 ? chapterEnd : undefined, + ); + + chapterText = chapterText.replace(/<script[^>]*>[\s\S]*?<\/script>/gim, ''); + + if (chapterText.includes('<img')) { + chapterText = chapterText.replace(/srcset="([^"]+)"/g, (match, src) => { + if (!src) return match; + const bestLink = src + .split(' ') + .filter((s: string) => s.startsWith('http')) + .pop(); + return bestLink ? `src="${bestLink}"` : match; + }); + } + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page = 1, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/vse-knigi/?searchname=${encodeURIComponent(searchTerm)}&bpage=${page}`; + return this.parseNovels(url); + } +} + +function parseStatus(statusString: string): string { + const s = statusString.toLowerCase().trim(); + + if ( + s.includes('активен') || + s.includes('продолжается') || + s.includes('онгоинг') + ) { + return NovelStatus.Ongoing; + } + + if (s.includes('завершен') || s.includes('конец') || s.includes('закончен')) { + return NovelStatus.Completed; + } + + if (s.includes('приостановлен') || s.includes('заморожен')) { + return NovelStatus.OnHiatus; + } + + return NovelStatus.Unknown; +} + +function parseDate(dateString = ''): string | null { + const months: Record<string, number> = { + января: 1, + февраля: 2, + марта: 3, + апреля: 4, + мая: 5, + июня: 6, + июля: 7, + августа: 8, + сентября: 9, + октября: 10, + ноября: 11, + декабря: 12, + }; + + // Checking the format "X ч. назад" + const relativeTimeRegex = /(d+)s*ч.?s*назад/; + const match = dateString.match(relativeTimeRegex); + if (match) { + const hoursAgo = parseInt(match[1], 10); + return dayjs().subtract(hoursAgo, 'hour').format('LL'); + } + + if (dateString.includes('.')) { + const [day, month, year] = dateString.split('.'); + const fullYear = year?.length === 2 ? '20' + year : year; + return dayjs(fullYear + '-' + month + '-' + day).format('LL'); + } else if (dateString.includes(' ')) { + const [day, month] = dateString.split(' '); + if (day && months[month]) { + const year = new Date().getFullYear(); + return dayjs(year + '-' + months[month] + '-' + day).format('LL'); + } + } + + return dateString || null; +} diff --git a/plugins/multisrc/lightnovelworld/generator.js b/plugins/multisrc/lightnovelworld/generator.js new file mode 100644 index 000000000..3b0f098b2 --- /dev/null +++ b/plugins/multisrc/lightnovelworld/generator.js @@ -0,0 +1,40 @@ +import list from './sources.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(source => { + const exist = existsSync(join(folder, 'filters', source.id + '.json')); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.id + '.json'), + ); + source.filters = JSON.parse(filters); + } + console.log( + `[lightnovelworld] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const LightNovelWPTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${LightNovelWPTemplate} +const plugin = new LightNovelWorld(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: source.options?.lang || 'English', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/lightnovelworld/sources.json b/plugins/multisrc/lightnovelworld/sources.json new file mode 100644 index 000000000..922937c4e --- /dev/null +++ b/plugins/multisrc/lightnovelworld/sources.json @@ -0,0 +1,17 @@ +[ + { + "id": "webnovelworld", + "sourceName": "Web Novel Pub", + "sourceSite": "https://www.webnovelworld.org/" + }, + { + "id": "lightnovelpubvip", + "sourceName": "LightNovelPub Vip", + "sourceSite": "https://lightnovelpub.vip/" + }, + { + "id": "lightnovelcave", + "sourceName": "LightNovelCave", + "sourceSite": "https://www.lightnovelcave.com/" + } +] diff --git a/plugins/multisrc/lightnovelworld/template.ts b/plugins/multisrc/lightnovelworld/template.ts new file mode 100644 index 000000000..162de9366 --- /dev/null +++ b/plugins/multisrc/lightnovelworld/template.ts @@ -0,0 +1,266 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; + +type LightNovelWorldOptions = { + lang?: string; + versionIncrements?: number; +}; + +export type LightNovelWorldMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: LightNovelWorldOptions; + filters?: Filters; +}; + +export class LightNovelWorld implements Plugin.PagePlugin { + id: string; + name: string; + site: string; + version: string; + icon: string; + headers = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + imageRequestInit?: Plugin.ImageRequestInit | undefined = { + headers: this.headers, + }; + options?: LightNovelWorldOptions; + + constructor(metadata: LightNovelWorldMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/lightnovelworld/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `1.0.${1 + versionIncrements}`; + this.options = metadata.options; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = `${this.site}browse/`; + link += `${filters.genres.value}/`; + link += `${filters.order.value}/`; + link += `${filters.status.value}/`; + link += page; + + const body = await fetchApi(link).then((r: Response) => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.novel-item.ads').remove(); + + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele) + .find('.novel-title > a') + .attr('href') + ?.substring(1); + + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl, + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const body = await fetchApi(this.site + novelPath).then((r: Response) => + r.text(), + ); + + const loadedCheerio = parseHTML(body); + const totalChapters = parseInt( + loadedCheerio('.header-stats span:first strong').text(), + 10, + ); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('h1.novel-title').text().trim() || 'Untitled', + cover: loadedCheerio('figure.cover > img').attr('data-src'), + author: loadedCheerio('.author > a > span').text(), + summary: loadedCheerio('.summary > .content').text().trim(), + status: loadedCheerio('.header-stats span:last strong').text(), + totalPages: Math.ceil(totalChapters / 100), + chapters: [], + }; + + novel.genres = loadedCheerio('.categories ul li') + .map((a, ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); + + return novel; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = this.site + novelPath + '/chapters/page-' + page; + const body = await fetchApi(url).then((res: Response) => res.text()); + const loadedCheerio = parseHTML(body); + const chapter: Plugin.ChapterItem[] = []; + loadedCheerio('.chapter-list li').each(function () { + const chapterName = + 'Chapter ' + + loadedCheerio(this).find('.chapter-no').text().trim() + + ' - ' + + loadedCheerio(this).find('.chapter-title').text().trim(); + + const releaseDate = loadedCheerio(this) + .find('.chapter-update') + .attr('datetime'); + + const chapterUrl = loadedCheerio(this) + .find('a') + .attr('href') + ?.substring(1); + if (!chapterUrl) return; + + chapter.push({ + name: chapterName, + path: chapterUrl, + releaseTime: dayjs(releaseDate).toISOString(), + }); + }); + const chapters = chapter; + return { chapters }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then((r: Response) => + r.text(), + ); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('#chapter-container').html() || ''; + + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}lnsearchlive`; + const link = `${this.site}search`; + const response = await fetchApi(link).then((r: Response) => r.text()); + const token = parseHTML(response); + const verifytoken = token('#novelSearchForm > input').attr('value'); + + const formData = new FormData(); + formData.append('inputContent', searchTerm); + + const results = await fetchApi(url, { + method: 'POST', + headers: { LNRequestVerifyToken: verifytoken! }, + body: formData, + }).then((r: Response) => r.json()); + + const novels: Plugin.NovelItem[] = []; + + const loadedCheerio = parseHTML(results.resultview); + + loadedCheerio('.novel-item').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h4.novel-title').text().trim(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + const novelUrl = loadedCheerio(ele).find('a').attr('href')?.substring(1); + if (!novelUrl) return; + novels.push({ + name: novelName, + path: novelUrl, + cover: novelCover, + }); + }); + + return novels; + } + + filters = { + order: { + value: 'popular', + label: 'Order by', + options: [ + { label: 'New', value: 'new' }, + { label: 'Popular', value: 'popular' }, + { label: 'Updates', value: 'updated' }, + ], + type: FilterTypes.Picker, + }, + status: { + value: 'all', + label: 'Status', + options: [ + { label: 'All', value: 'all' }, + { label: 'Completed', value: 'completed' }, + { label: 'Ongoing', value: 'ongoing' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: 'all', + label: 'Genre', + options: [ + { label: 'All', value: 'all' }, + { label: 'Action', value: 'action' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Drama', value: 'drama' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Harem', value: 'harem' }, + { label: 'Martial Arts', value: 'martial-arts' }, + { label: 'Mature', value: 'mature' }, + { label: 'Romance', value: 'romance' }, + { label: 'Tragedy', value: 'tragedy' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'Slice of Life', value: 'slice-of-life' }, + { label: 'Mystery', value: 'mystery' }, + { label: 'Supernatural', value: 'supernatural' }, + { label: 'Psychological', value: 'psychological' }, + { label: 'Sci-fi', value: 'sci-fi' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Josei', value: 'josei' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Horror', value: 'horror' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Historical', value: 'historical' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Adult', value: 'adult' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Sports', value: 'sports' }, + { label: 'Lolicon', value: 'lolicon' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Shounen Ai', value: 'shounen-ai' }, + { label: 'Yaoi', value: 'yaoi' }, + { label: 'Video Games', value: 'video-games' }, + { label: 'Smut', value: 'smut' }, + { label: 'Magical Realism', value: 'magical-realism' }, + { label: 'Eastern Fantasy', value: 'eastern-fantasy' }, + { label: 'Contemporary Romance', value: 'contemporary-romance' }, + { label: 'Fantasy Romance', value: 'fantasy-romance' }, + { label: 'Shoujo Ai', value: 'shoujo-ai' }, + { label: 'Yuri', value: 'yuri' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} diff --git a/plugins/multisrc/lightnovelwp/README.md b/plugins/multisrc/lightnovelwp/README.md new file mode 100644 index 000000000..f665fc4f1 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/README.md @@ -0,0 +1,49 @@ +# LightnovelWP multisrc generator + +## Compatiblity + +### This generator is for most sites that uses the LightNovel WordPress Theme: https://themesia.com/lightnovel-wordpress-theme/ + +it should work for all sites that uses this theme (highest version tested: 1.1.5) + +to know the name and version of the theme you can enter the url in : [WP Theme Detector](https://www.wpthemedetector.com) +(it sometimes hallucinates for version 1.0.X as 1.0) \ +or you can check the version by adding "/wp-content/themes/lightnovel/style.css" +to the site url and check the version in the file + +## Add a new source + +### sources.json + +To add a new source you need to add it to sources.json: + +- id: the id of the source (something unique) +- sourceName: the name of the source (you can use the value of "name" in "https://site.com/wp-json/" if it exists) +- sourceSite: the site url +- options: the options of the source + - lang: the language of the source (default: "English") (check that the language + exists in the languages (check folder names in "plugins/")) + - reverseChapter: if the chapters are in reverse order + (if the first chapter of the list is 1 set it to false) + - versionIncrements: needs to be incremented if the url is changed or customJS + is changed or a change to the template affects only this source + - seriesPath: the path to the series page (if the "Novels"/"All Series" button + doesn't go to /series/ (ex: https://site.com/novels/ set it to "/novels/")) + - customJS: custom javascript that will be excuted when getting the text (if + the site has a custom copyright that need to be removed (the cherrio is $)) + +### icon + +To add an icon to the source you can just run `npm run build:icons` to generate all icons (make sure `npm run build:multisrc` works on your machine) + +Or you can manualy find the icon of the site \ +(try the favicon of the site (https://site.com/favicon.ico) most of the times it redirects you to an image named something like "cropped-site-32x32.png" try to access "cropped-site.png" or "site.png" if that did not work you can try to access "https://site.com/wp-json/" at the end of this very long file there should be a "site_icon_url" value +) (don't forget to convert it to png) +and add it to the folder "public/static/multisrc/lightnovelwp/{sourceID}/icon.png" + +### filters + +To add filters to a source you need to run the script "get_filters.js" \ +(`npx node plugins/multisrc/lightnovelwp/get_filters.js` +(if you are at the root of the project) (and you have ran "npm install" before)) +and follow the instructions (url is easier and faster but sometimes it doesn't work) diff --git a/plugins/multisrc/lightnovelwp/filters/TCSega.json b/plugins/multisrc/lightnovelwp/filters/TCSega.json new file mode 100644 index 000000000..14960839e --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/TCSega.json @@ -0,0 +1,196 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Género", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipo", + "value": [], + "options": [ + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Published Novel (JP)", + "value": "published-novel-jp" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Estado", + "value": "", + "options": [ + { + "label": "Todos", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Ordenar por", + "value": "", + "options": [ + { + "label": "Pred.", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Actualización / Latest Release", + "value": "update" + }, + { + "label": "Último Agregado", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Puntuación", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/allnovelread.json b/plugins/multisrc/lightnovelwp/filters/allnovelread.json new file mode 100644 index 000000000..7c580392d --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/allnovelread.json @@ -0,0 +1,1076 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "16+", + "value": "16" + }, + { + "label": "Abogado/Abogada", + "value": "abogado-abogada" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Advogdo", + "value": "advogdo" + }, + { + "label": "affair of the heart", + "value": "affair-of-the-heart" + }, + { + "label": "alfa", + "value": "alfa" + }, + { + "label": "Alpha", + "value": "alpha" + }, + { + "label": "Amable", + "value": "amable" + }, + { + "label": "Amar", + "value": "amar" + }, + { + "label": "Amor", + "value": "amor" + }, + { + "label": "Amor caliente", + "value": "amor-caliente" + }, + { + "label": "amor depois do casamento", + "value": "amor-depois-do-casamento" + }, + { + "label": "Amor después del matrimonio", + "value": "amor-despues-del-matrimonio" + }, + { + "label": "Amor destinado", + "value": "amor-destinado" + }, + { + "label": "Amor doloroso", + "value": "amor-doloroso" + }, + { + "label": "Amor dulce", + "value": "amor-dulce" + }, + { + "label": "Amor e ódio", + "value": "amor-e-odio" + }, + { + "label": "Amor e ódio Gravidez", + "value": "amor-e-odio-gravidez" + }, + { + "label": "amor entre ex", + "value": "amor-entre-ex" + }, + { + "label": "Amor forzado", + "value": "amor-forzado" + }, + { + "label": "Amor Inocente", + "value": "amor-inocente" + }, + { + "label": "amor predestinado", + "value": "amor-predestinado" + }, + { + "label": "Amor y odio", + "value": "amor-y-odio" + }, + { + "label": "arrepentirse del divorcio", + "value": "arrepentirse-del-divorcio" + }, + { + "label": "Arrepentirse Después de Herir a Su Mujer", + "value": "arrepentirse-despues-de-herir-a-su-mujer" + }, + { + "label": "Arrogante", + "value": "arrogante" + }, + { + "label": "Asesinato", + "value": "asesinato" + }, + { + "label": "Babby", + "value": "babby" + }, + { + "label": "BABY", + "value": "baby" + }, + { + "label": "Beauty", + "value": "beauty" + }, + { + "label": "Bebê fofo", + "value": "bebe-fofo" + }, + { + "label": "Bebé inteligente", + "value": "bebe-inteligente" + }, + { + "label": "belleza", + "value": "belleza" + }, + { + "label": "Belleza inusual", + "value": "belleza-inusual" + }, + { + "label": "Bilionário", + "value": "bilionario" + }, + { + "label": "Billionair", + "value": "billionair" + }, + { + "label": "billionaire", + "value": "billionaire" + }, + { + "label": "Billonario/Billonaria", + "value": "billonario-billonaria" + }, + { + "label": "brilliant", + "value": "brilliant" + }, + { + "label": "bxg", + "value": "bxg" + }, + { + "label": "Bxg-novela", + "value": "bxg-novela" + }, + { + "label": "Campus", + "value": "campus" + }, + { + "label": "Casamiento", + "value": "casamiento" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "city", + "value": "city" + }, + { + "label": "Colegiala", + "value": "colegiala" + }, + { + "label": "Comedia", + "value": "comedia" + }, + { + "label": "Comedia-novela", + "value": "comedia-novela" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "contemporáneo", + "value": "contemporaneo" + }, + { + "label": "Contract marriage", + "value": "contract-marriage" + }, + { + "label": "cónyuge", + "value": "conyuge" + }, + { + "label": "Corazón roto", + "value": "corazon-roto" + }, + { + "label": "courtship", + "value": "courtship" + }, + { + "label": "Crecimiento del personaje", + "value": "crecimiento-del-personaje" + }, + { + "label": "Crimen organizado", + "value": "crimen-organizado" + }, + { + "label": "Critical", + "value": "critical" + }, + { + "label": "cruel", + "value": "cruel" + }, + { + "label": "De pobre a rico", + "value": "de-pobre-a-rico" + }, + { + "label": "Divertido", + "value": "divertido" + }, + { + "label": "Divorce", + "value": "divorce" + }, + { + "label": "Divorcio", + "value": "divorcio" + }, + { + "label": "Doce", + "value": "doce" + }, + { + "label": "Doctor", + "value": "doctor" + }, + { + "label": "Dominador", + "value": "dominador" + }, + { + "label": "Dominante", + "value": "dominante" + }, + { + "label": "Dominante-novela", + "value": "dominante-novela" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "dulce", + "value": "dulce" + }, + { + "label": "Dulce Embarazada", + "value": "dulce-embarazada" + }, + { + "label": "elegante", + "value": "elegante" + }, + { + "label": "Embarazada", + "value": "embarazada" + }, + { + "label": "En la actualidad", + "value": "en-la-actualidad" + }, + { + "label": "Enemigos a los amantes", + "value": "enemigos-a-los-amantes" + }, + { + "label": "existente", + "value": "existente" + }, + { + "label": "Family", + "value": "family" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fated", + "value": "fated" + }, + { + "label": "Fraco para forte/Pob", + "value": "fraco-para-forte-pob" + }, + { + "label": "fuerte", + "value": "fuerte" + }, + { + "label": "Goodgirl", + "value": "goodgirl" + }, + { + "label": "Gravidez", + "value": "gravidez" + }, + { + "label": "HE", + "value": "he" + }, + { + "label": "heir/heiress", + "value": "heir-heiress" + }, + { + "label": "hermoso", + "value": "hermoso" + }, + { + "label": "Héroe pateador", + "value": "heroe-pateador" + }, + { + "label": "Heroina", + "value": "heroina" + }, + { + "label": "heroína Kickass", + "value": "heroina-kickass" + }, + { + "label": "heterose*ual", + "value": "heteroseual" + }, + { + "label": "historia de amor", + "value": "historia-de-amor" + }, + { + "label": "Hot Romance", + "value": "hot-romance" + }, + { + "label": "Humor", + "value": "humor" + }, + { + "label": "Identidad secreta", + "value": "identidad-secreta" + }, + { + "label": "Independente", + "value": "independente" + }, + { + "label": "Independiente", + "value": "independiente" + }, + { + "label": "Inocente", + "value": "inocente" + }, + { + "label": "jefe", + "value": "jefe" + }, + { + "label": "Jefe / CEO", + "value": "jefe-ceo" + }, + { + "label": "kicking", + "value": "kicking" + }, + { + "label": "king", + "value": "king" + }, + { + "label": "legend", + "value": "legend" + }, + { + "label": "Literature", + "value": "literature" + }, + { + "label": "loser", + "value": "loser" + }, + { + "label": "Love", + "value": "love" + }, + { + "label": "Love & Culture", + "value": "love-culture" + }, + { + "label": "love after marriage", + "value": "love-after-marriage" + }, + { + "label": "love story", + "value": "love-story" + }, + { + "label": "LOVEAFTERMARRIAGE", + "value": "loveaftermarriage" + }, + { + "label": "lucky dog", + "value": "lucky-dog" + }, + { + "label": "Lugar para você Allnovelread", + "value": "lugar-para-voce-allnovelread" + }, + { + "label": "luna", + "value": "luna" + }, + { + "label": "Madre soltera", + "value": "madre-soltera" + }, + { + "label": "Mafia", + "value": "mafia" + }, + { + "label": "magical world", + "value": "magical-world" + }, + { + "label": "Malentendido", + "value": "malentendido" + }, + { + "label": "Maquinación", + "value": "maquinacion" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Matrimonio", + "value": "matrimonio" + }, + { + "label": "Matrimonio por Contrato", + "value": "matrimonio-por-contrato" + }, + { + "label": "Matrimonio relámpago", + "value": "matrimonio-relampago" + }, + { + "label": "Medico", + "value": "medico" + }, + { + "label": "Médico/Médica", + "value": "medico-medica" + }, + { + "label": "millonaria", + "value": "millonaria" + }, + { + "label": "modificación", + "value": "modificacion" + }, + { + "label": "most millions", + "value": "most-millions" + }, + { + "label": "Mucama", + "value": "mucama" + }, + { + "label": "Mujer súper poderosa", + "value": "mujer-super-poderosa" + }, + { + "label": "Multi-Millionairo", + "value": "multi-millionairo" + }, + { + "label": "Multimillionairo", + "value": "multimillionairo" + }, + { + "label": "Multimillonaria", + "value": "multimillonaria" + }, + { + "label": "multimillonario", + "value": "multimillonario" + }, + { + "label": "Multimillonario-novela", + "value": "multimillonario-novela" + }, + { + "label": "MULTIPLEIDENTITIES", + "value": "multipleidentities" + }, + { + "label": "Múltiples identidades", + "value": "multiples-identidades" + }, + { + "label": "musculoso", + "value": "musculoso" + }, + { + "label": "Nacimiento múltiple", + "value": "nacimiento-multiple" + }, + { + "label": "Novia embarazada a la fuga", + "value": "novia-embarazada-a-la-fuga" + }, + { + "label": "Obsesión", + "value": "obsesion" + }, + { + "label": "Ocultar", + "value": "ocultar" + }, + { + "label": "Optimista", + "value": "optimista" + }, + { + "label": "others", + "value": "others" + }, + { + "label": "Pasión de una noche", + "value": "pasion-de-una-noche" + }, + { + "label": "Perao/Segunda chance", + "value": "perao-segunda-chance" + }, + { + "label": "Perdedor", + "value": "perdedor" + }, + { + "label": "Playboy", + "value": "playboy" + }, + { + "label": "poderoso", + "value": "poderoso" + }, + { + "label": "polygamy", + "value": "polygamy" + }, + { + "label": "Posesivo", + "value": "posesivo" + }, + { + "label": "possessive", + "value": "possessive" + }, + { + "label": "Possessivo", + "value": "possessivo" + }, + { + "label": "Powerful", + "value": "powerful" + }, + { + "label": "presente", + "value": "presente" + }, + { + "label": "Presidente", + "value": "presidente" + }, + { + "label": "princess", + "value": "princess" + }, + { + "label": "Protective", + "value": "protective" + }, + { + "label": "Protectormadre soltera", + "value": "protectormadre-soltera" + }, + { + "label": "Reconquistar a mi pareja", + "value": "reconquistar-a-mi-pareja" + }, + { + "label": "rejected", + "value": "rejected" + }, + { + "label": "relación", + "value": "relacion" + }, + { + "label": "relationship", + "value": "relationship" + }, + { + "label": "Renacido", + "value": "renacido" + }, + { + "label": "Rey/Reina", + "value": "rey-reina" + }, + { + "label": "Rich", + "value": "rich" + }, + { + "label": "Rico", + "value": "rico" + }, + { + "label": "Ricos", + "value": "ricos" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "romance caliente", + "value": "romance-caliente" + }, + { + "label": "Romance/Romântico", + "value": "romance-romantico" + }, + { + "label": "Romántic", + "value": "romantic" + }, + { + "label": "Romantica", + "value": "romantica" + }, + { + "label": "Romanticas", + "value": "romanticas" + }, + { + "label": "Romantico", + "value": "romantico" + }, + { + "label": "Secretos", + "value": "secretos" + }, + { + "label": "secrets", + "value": "secrets" + }, + { + "label": "seductive", + "value": "seductive" + }, + { + "label": "Segunda Chance", + "value": "segunda-chance" + }, + { + "label": "Segunda oportunidad", + "value": "segunda-oportunidad" + }, + { + "label": "STRONGFEMALELEAD", + "value": "strongfemalelead" + }, + { + "label": "Subrogación", + "value": "subrogacion" + }, + { + "label": "Suspense", + "value": "suspense" + }, + { + "label": "Sweet", + "value": "sweet" + }, + { + "label": "SWEETLOVE", + "value": "sweetlove" + }, + { + "label": "Teenager", + "value": "teenager" + }, + { + "label": "Tierno", + "value": "tierno" + }, + { + "label": "Tragedia", + "value": "tragedia" + }, + { + "label": "Traición", + "value": "traicion" + }, + { + "label": "TraiciónReconquistar a mi pareja", + "value": "traicionreconquistar-a-mi-pareja" + }, + { + "label": "Triángulo amoroso", + "value": "triangulo-amoroso" + }, + { + "label": "Trillizos", + "value": "trillizos" + }, + { + "label": "Trio", + "value": "trio" + }, + { + "label": "Una noche de pasion", + "value": "una-noche-de-pasion" + }, + { + "label": "Universidad", + "value": "universidad" + }, + { + "label": "Valente", + "value": "valente" + }, + { + "label": "Valiente", + "value": "valiente" + }, + { + "label": "Venanza", + "value": "venanza" + }, + { + "label": "Werewolf", + "value": "werewolf" + }, + { + "label": "Ya", + "value": "ya" + }, + { + "label": "Youth", + "value": "youth" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "16+", + "value": "16" + }, + { + "label": "alfa", + "value": "alfa" + }, + { + "label": "Allnovelread Sin vuelta atrás", + "value": "allnovelread-sin-vuelta-atras" + }, + { + "label": "Alpha", + "value": "alpha" + }, + { + "label": "Amor dulce", + "value": "amor-dulce" + }, + { + "label": "Amor y odio", + "value": "amor-y-odio" + }, + { + "label": "Arrogante-novela", + "value": "arrogante-novela" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "Billonario", + "value": "billonario" + }, + { + "label": "bxg", + "value": "bxg" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "Contemporâneo", + "value": "contemporaneo" + }, + { + "label": "Contract marriage", + "value": "contract-marriage" + }, + { + "label": "crecimiento-del-personaje-novela", + "value": "crecimiento-del-personaje-novela" + }, + { + "label": "Divorce", + "value": "divorce" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "dulce", + "value": "dulce" + }, + { + "label": "El incesante acoso de mi ex marido", + "value": "el-incesante-acoso-de-mi-ex-marido" + }, + { + "label": "Enganar al mejor amigo de mi novio", + "value": "enganar-al-mejor-amigo-de-mi-novio" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "HE", + "value": "he" + }, + { + "label": "heterosexual", + "value": "heterosexual" + }, + { + "label": "Historia-triste-novela", + "value": "historia-triste-novela" + }, + { + "label": "Hombre lobo", + "value": "hombre-lobo" + }, + { + "label": "Hot Romance", + "value": "hot-romance" + }, + { + "label": "Independiente", + "value": "independiente" + }, + { + "label": "Inocente", + "value": "inocente" + }, + { + "label": "king", + "value": "king" + }, + { + "label": "Love", + "value": "love" + }, + { + "label": "love after marriage", + "value": "love-after-marriage" + }, + { + "label": "Luna", + "value": "luna" + }, + { + "label": "Magical world", + "value": "magical-world" + }, + { + "label": "millonaria", + "value": "millonaria" + }, + { + "label": "Multi-Millionaire", + "value": "multi-millionaire" + }, + { + "label": "Multimillionairo", + "value": "multimillionairo" + }, + { + "label": "Multimillonario", + "value": "multimillonario" + }, + { + "label": "Nunca Longe Allnovelread", + "value": "nunca-longe-allnovelread" + }, + { + "label": "Posesivo", + "value": "posesivo" + }, + { + "label": "Querida ex esposa", + "value": "querida-ex-esposa" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romane", + "value": "romane" + }, + { + "label": "Romántica", + "value": "romantica" + }, + { + "label": "Romanticas", + "value": "romanticas" + }, + { + "label": "Romantico", + "value": "romantico" + }, + { + "label": "Sweet", + "value": "sweet" + }, + { + "label": "SWEETLOVE", + "value": "sweetlove" + }, + { + "label": "Te Quero de Volta Allnovelread", + "value": "te-quero-de-volta-allnovelread" + }, + { + "label": "Traicion en altar", + "value": "traicion-en-altar" + }, + { + "label": "Uma Ferida Que Nunca Se Cura Allnovelread", + "value": "uma-ferida-que-nunca-se-cura-allnovelread" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban/Realistic", + "value": "urban-realistic" + }, + { + "label": "vuelva a mí", + "value": "vuelva-a-mi" + }, + { + "label": "Werewolf", + "value": "werewolf" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/arcane.json b/plugins/multisrc/lightnovelwp/filters/arcane.json new file mode 100644 index 000000000..baf103cec --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/arcane.json @@ -0,0 +1,232 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cyberpunk", + "value": "cyberpunk" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fusion", + "value": "fusion" + }, + { + "label": "Game World", + "value": "game-world" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Misunderstandings", + "value": "misunderstandings" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Obsession", + "value": "obsession" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Regret", + "value": "regret" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "War", + "value": "war" + }, + { + "label": "Wuxia", + "value": "wuxia" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Japanese Novel", + "value": "japanese-novel" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/bacalightnovel.json b/plugins/multisrc/lightnovelwp/filters/bacalightnovel.json new file mode 100644 index 000000000..d5bb169fc --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/bacalightnovel.json @@ -0,0 +1,236 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Antihero", + "value": "antihero" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Superhero", + "value": "superhero" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipe", + "value": [], + "options": [ + { + "label": "", + "value": "" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Novel", + "value": "novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Urutkan", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Baru diperbarui", + "value": "update" + }, + { + "label": "Baru ditambahkan", + "value": "latest" + }, + { + "label": "Terpopuler", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/centralnovel.json b/plugins/multisrc/lightnovelwp/filters/centralnovel.json new file mode 100644 index 000000000..2c331c98f --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/centralnovel.json @@ -0,0 +1,312 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Gênero", + "value": [], + "options": [ + { + "label": "Ação", + "value": "acao" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adulto", + "value": "adulto" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Artes Marciais", + "value": "artes-marciais" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Comédia", + "value": "comedia" + }, + { + "label": "Cotidiano", + "value": "cotidiano" + }, + { + "label": "Cultivo", + "value": "cultivo" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Escolar", + "value": "escolar" + }, + { + "label": "Esportes", + "value": "esportes" + }, + { + "label": "Evolução", + "value": "evolucao" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Ficção Científica", + "value": "ficcao-cientifica" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harém", + "value": "harem" + }, + { + "label": "Histórico", + "value": "historico" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "LitRPG", + "value": "litrpg" + }, + { + "label": "Magia", + "value": "magia" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Mitologia", + "value": "mitologia" + }, + { + "label": "Monstros", + "value": "monstros" + }, + { + "label": "Pet", + "value": "pet" + }, + { + "label": "Protagonista Feminina", + "value": "protagonista-feminina" + }, + { + "label": "Protagonista Maligno", + "value": "protagonista-maligno" + }, + { + "label": "Psicológico", + "value": "psicologico" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reencarnação", + "value": "reencarnacao" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen BL", + "value": "shounen-bl" + }, + { + "label": "Sistema", + "value": "sistema" + }, + { + "label": "Sistema de Jogo", + "value": "sistema-de-jogo" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sobrenatural", + "value": "sobrenatural" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Terror", + "value": "terror" + }, + { + "label": "Tragédia", + "value": "tragedia" + }, + { + "label": "Transmigração", + "value": "transmigracao" + }, + { + "label": "Vida Escolar", + "value": "vida-escolar" + }, + { + "label": "VRMMO", + "value": "vrmmo" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipo", + "value": [], + "options": [ + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Novel Chinesa", + "value": "novel-chinesa" + }, + { + "label": "Novel Coreana", + "value": "novel-coreana" + }, + { + "label": "Novel Japonesa", + "value": "novel-japonesa" + }, + { + "label": "Novel Ocidental", + "value": "novel-ocidental" + }, + { + "label": "Webnovel", + "value": "webnovel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "Todos", + "value": "" + }, + { + "label": "Em andamento", + "value": "em andamento" + }, + { + "label": "Hiato", + "value": "hiato" + }, + { + "label": "Completo", + "value": "completo" + } + ] + }, + "order": { + "type": "Picker", + "label": "Ordenar por", + "value": "", + "options": [ + { + "label": "Padrão", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Últ. Att", + "value": "update" + }, + { + "label": "Últ. Add", + "value": "latest" + }, + { + "label": "Populares", + "value": "popular" + }, + { + "label": "Avaliação", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/cpunovel.json b/plugins/multisrc/lightnovelwp/filters/cpunovel.json new file mode 100644 index 000000000..85d163833 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/cpunovel.json @@ -0,0 +1,324 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary", + "value": "contemporary" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Family", + "value": "family" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Growth", + "value": "growth" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hidden Identity", + "value": "hidden-identity" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Overpowered", + "value": "overpowered" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Rich", + "value": "rich" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Son-in-Law", + "value": "son-in-law" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Superpower", + "value": "superpower" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Virtual Game", + "value": "virtual-game" + }, + { + "label": "Warrior", + "value": "warrior" + }, + { + "label": "Webtoons", + "value": "webtoons" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/daotranslate.json b/plugins/multisrc/lightnovelwp/filters/daotranslate.json new file mode 100644 index 000000000..85d163833 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/daotranslate.json @@ -0,0 +1,324 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary", + "value": "contemporary" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Family", + "value": "family" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Growth", + "value": "growth" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hidden Identity", + "value": "hidden-identity" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Overpowered", + "value": "overpowered" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Rich", + "value": "rich" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Son-in-Law", + "value": "son-in-law" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Superpower", + "value": "superpower" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Virtual Game", + "value": "virtual-game" + }, + { + "label": "Warrior", + "value": "warrior" + }, + { + "label": "Webtoons", + "value": "webtoons" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/ellotl.json b/plugins/multisrc/lightnovelwp/filters/ellotl.json new file mode 100644 index 000000000..bfd63e9fc --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/ellotl.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern Fantasy", + "value": "modern-fantasy" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Genius", + "value": "genius" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "Japanese Web Novel", + "value": "japanese-web-novel" + }, + { + "label": "Korean Web Novel", + "value": "korean-web-novel" + }, + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Mage", + "value": "mage" + }, + { + "label": "Modern Fantasy", + "value": "modern-fantasy" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/ippotranslations.json b/plugins/multisrc/lightnovelwp/filters/ippotranslations.json new file mode 100644 index 000000000..d52da90d1 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/ippotranslations.json @@ -0,0 +1,292 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "character growth", + "value": "character-growth" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Dark Fantasy", + "value": "dark-fantasy" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Finalized", + "value": "finalized" + }, + { + "label": "Fusion", + "value": "fusion" + }, + { + "label": "Fusion Fantasy", + "value": "fusion-fantasy" + }, + { + "label": "Game-based(LitRPG)", + "value": "game-basedlitrpg" + }, + { + "label": "Girl's Love", + "value": "girls-love" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "hero", + "value": "hero" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Humiliation", + "value": "humiliation" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "level up", + "value": "level-up" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Male protagonist", + "value": "male-protagonist" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Political Intrigue", + "value": "political-intrigue" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Pure love", + "value": "pure-love" + }, + { + "label": "R18", + "value": "r18" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fy", + "value": "sci-fy" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "sword", + "value": "sword" + }, + { + "label": "system", + "value": "system" + }, + { + "label": "Teacher", + "value": "teacher" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yandere", + "value": "yandere" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Dropped", + "value": "dropped" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/kdtnovels.json b/plugins/multisrc/lightnovelwp/filters/kdtnovels.json new file mode 100644 index 000000000..3675b0c72 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/kdtnovels.json @@ -0,0 +1,172 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Genderswap", + "value": "genderswap" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Monster Girls", + "value": "monster-girls" + }, + { + "label": "Monsters", + "value": "monsters" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Survival", + "value": "survival" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/keopi.json b/plugins/multisrc/lightnovelwp/filters/keopi.json new file mode 100644 index 000000000..858d5648b --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/keopi.json @@ -0,0 +1,116 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Smut", + "value": "smut" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/keopitranslations.json b/plugins/multisrc/lightnovelwp/filters/keopitranslations.json new file mode 100644 index 000000000..858d5648b --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/keopitranslations.json @@ -0,0 +1,116 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Smut", + "value": "smut" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/knoxt.json b/plugins/multisrc/lightnovelwp/filters/knoxt.json new file mode 100644 index 000000000..eb6a8c80a --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/knoxt.json @@ -0,0 +1,1148 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "1v1", + "value": "1v1" + }, + { + "label": "ABO", + "value": "abo" + }, + { + "label": "Absent Parents", + "value": "absent-parents" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adapted to Drama CD", + "value": "adapted-to-drama-cd" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adults", + "value": "adults" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Adventurers", + "value": "adventurers" + }, + { + "label": "Age gap", + "value": "age-gap" + }, + { + "label": "Age Regression", + "value": "age-regression" + }, + { + "label": "Aggressive Characters", + "value": "aggressive-characters" + }, + { + "label": "Amnesia", + "value": "amnesia" + }, + { + "label": "Ancient times", + "value": "ancient-times" + }, + { + "label": "Anti-social Protagonist", + "value": "anti-social-protagonist" + }, + { + "label": "Appearance Changes", + "value": "appearance-changes" + }, + { + "label": "Arranged Marriage", + "value": "arranged-marriage" + }, + { + "label": "Arrogant Characters", + "value": "arrogant-characters" + }, + { + "label": "Artificial Intelligence", + "value": "artificial-intelligence" + }, + { + "label": "Artists", + "value": "artists" + }, + { + "label": "Beautiful bottom", + "value": "beautiful-bottom" + }, + { + "label": "Betrayal", + "value": "betrayal" + }, + { + "label": "Bickering Couple", + "value": "bickering-couple" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "BL (Boys' Love)", + "value": "bl-boys-love" + }, + { + "label": "Blind Dates", + "value": "blind-dates" + }, + { + "label": "Blind Protagonist", + "value": "blind-protagonist" + }, + { + "label": "book wearing", + "value": "book-wearing" + }, + { + "label": "Boys love", + "value": "boys-love" + }, + { + "label": "Business Management", + "value": "business-management" + }, + { + "label": "Businessmen", + "value": "businessmen" + }, + { + "label": "Calm Protagonist", + "value": "calm-protagonist" + }, + { + "label": "Campus", + "value": "campus" + }, + { + "label": "carefree protagonist", + "value": "carefree-protagonist" + }, + { + "label": "Caring Protagonist", + "value": "caring-protagonist" + }, + { + "label": "celebrity", + "value": "celebrity" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "Character Growth", + "value": "character-growth" + }, + { + "label": "Charismatic Protagonist", + "value": "charismatic-protagonist" + }, + { + "label": "Charming Protagonist", + "value": "charming-protagonist" + }, + { + "label": "Child Abuse", + "value": "child-abuse" + }, + { + "label": "Childcare", + "value": "childcare" + }, + { + "label": "Childhood Friends", + "value": "childhood-friends" + }, + { + "label": "Childhood Love", + "value": "childhood-love" + }, + { + "label": "Childish Protagonist", + "value": "childish-protagonist" + }, + { + "label": "Chinese novel", + "value": "chinese-novel" + }, + { + "label": "Clever Protagonist", + "value": "clever-protagonist" + }, + { + "label": "Clingy Lover", + "value": "clingy-lover" + }, + { + "label": "Clumsy Love Interests", + "value": "clumsy-love-interests" + }, + { + "label": "Cohabitation", + "value": "cohabitation" + }, + { + "label": "Cold Love Interests", + "value": "cold-love-interests" + }, + { + "label": "Cold Protagonist", + "value": "cold-protagonist" + }, + { + "label": "Comdey", + "value": "comdey" + }, + { + "label": "comedic undertone", + "value": "comedic-undertone" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Complex Family Relationships", + "value": "complex-family-relationships" + }, + { + "label": "Confident Protagonist", + "value": "confident-protagonist" + }, + { + "label": "Conflicting Loyalties", + "value": "conflicting-loyalties" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Couple Growth", + "value": "couple-growth" + }, + { + "label": "Crime", + "value": "crime" + }, + { + "label": "Cross-dressing", + "value": "cross-dressing" + }, + { + "label": "Cryostasis", + "value": "cryostasis" + }, + { + "label": "Cute Children", + "value": "cute-children" + }, + { + "label": "Cute Protagonist", + "value": "cute-protagonist" + }, + { + "label": "Cute Story", + "value": "cute-story" + }, + { + "label": "Death of Loved Ones", + "value": "death-of-loved-ones" + }, + { + "label": "Determined Protagonist", + "value": "determined-protagonist" + }, + { + "label": "Devoted Love Interests", + "value": "devoted-love-interests" + }, + { + "label": "Different Social Status", + "value": "different-social-status" + }, + { + "label": "Divorce", + "value": "divorce" + }, + { + "label": "Doctors", + "value": "doctors" + }, + { + "label": "Doting love interest", + "value": "doting-love-interest" + }, + { + "label": "Doting Love Interests", + "value": "doting-love-interests" + }, + { + "label": "Doting Older Siblings", + "value": "doting-older-siblings" + }, + { + "label": "Doting Parents", + "value": "doting-parents" + }, + { + "label": "Double AA", + "value": "double-aa" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dystopia", + "value": "dystopia" + }, + { + "label": "empowerment fiction", + "value": "empowerment-fiction" + }, + { + "label": "Enemies Become Lovers", + "value": "enemies-become-lovers" + }, + { + "label": "Entertaiment circle", + "value": "entertaiment-circle" + }, + { + "label": "Entertainment circle", + "value": "entertainment-circle" + }, + { + "label": "Evolution", + "value": "evolution" + }, + { + "label": "F*llatio", + "value": "fllatio" + }, + { + "label": "Family Conflict", + "value": "family-conflict" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Farming", + "value": "farming" + }, + { + "label": "Fated Lovers", + "value": "fated-lovers" + }, + { + "label": "Fearless Protagonist", + "value": "fearless-protagonist" + }, + { + "label": "Fiction", + "value": "fiction" + }, + { + "label": "Fictional", + "value": "fictional" + }, + { + "label": "First Love", + "value": "first-love" + }, + { + "label": "First-time Interc**rse", + "value": "first-time-intercrse" + }, + { + "label": "futuristic setting", + "value": "futuristic-setting" + }, + { + "label": "Gaming", + "value": "gaming" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "General", + "value": "general" + }, + { + "label": "Genetic Modifications", + "value": "genetic-modifications" + }, + { + "label": "GL", + "value": "gl" + }, + { + "label": "Gore", + "value": "gore" + }, + { + "label": "Handsome Male Lead", + "value": "handsome-male-lead" + }, + { + "label": "Hard-Working Protagonist", + "value": "hard-working-protagonist" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "HE", + "value": "he" + }, + { + "label": "Heartwarming", + "value": "heartwarming" + }, + { + "label": "Hiding True Abilities", + "value": "hiding-true-abilities" + }, + { + "label": "Hiding True Identity", + "value": "hiding-true-identity" + }, + { + "label": "historic love", + "value": "historic-love" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Human-Nonhuman Relationship", + "value": "human-nonhuman-relationship" + }, + { + "label": "humor", + "value": "humor" + }, + { + "label": "Idol", + "value": "idol" + }, + { + "label": "Inferiority Complex", + "value": "inferiority-complex" + }, + { + "label": "infrastructure", + "value": "infrastructure" + }, + { + "label": "interstellar", + "value": "interstellar" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Jealousy", + "value": "jealousy" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Kind love interest", + "value": "kind-love-interest" + }, + { + "label": "Kind Love Interests", + "value": "kind-love-interests" + }, + { + "label": "Lawyers", + "value": "lawyers" + }, + { + "label": "lighthearted drama", + "value": "lighthearted-drama" + }, + { + "label": "Loner Protagonist", + "value": "loner-protagonist" + }, + { + "label": "Long Separations", + "value": "long-separations" + }, + { + "label": "Love Affair", + "value": "love-affair" + }, + { + "label": "Love and hate", + "value": "love-and-hate" + }, + { + "label": "love at first sight", + "value": "love-at-first-sight" + }, + { + "label": "Love Interest Falls in Love First", + "value": "love-interest-falls-in-love-first" + }, + { + "label": "love romance", + "value": "love-romance" + }, + { + "label": "Lovers Reunited", + "value": "lovers-reunited" + }, + { + "label": "Male protagonist", + "value": "male-protagonist" + }, + { + "label": "Male Yandere", + "value": "male-yandere" + }, + { + "label": "Manipulative Characters", + "value": "manipulative-characters" + }, + { + "label": "Manly Gay Couple", + "value": "manly-gay-couple" + }, + { + "label": "Marriage", + "value": "marriage" + }, + { + "label": "Marriage of Convenience", + "value": "marriage-of-convenience" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mary Sue", + "value": "mary-sue" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medical Knowledge", + "value": "medical-knowledge" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Misunderstandings", + "value": "misunderstandings" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Modern day", + "value": "modern-day" + }, + { + "label": "Mpreg", + "value": "mpreg" + }, + { + "label": "Multiple Reincarnated Individuals", + "value": "multiple-reincarnated-individuals" + }, + { + "label": "Multiple worlds", + "value": "multiple-worlds" + }, + { + "label": "Music", + "value": "music" + }, + { + "label": "Mute Character", + "value": "mute-character" + }, + { + "label": "mutual crush", + "value": "mutual-crush" + }, + { + "label": "mutual salvation", + "value": "mutual-salvation" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Mystery Solving", + "value": "mystery-solving" + }, + { + "label": "Naive Protagonist", + "value": "naive-protagonist" + }, + { + "label": "Near-Death Experience", + "value": "near-death-experience" + }, + { + "label": "Obsessive Love", + "value": "obsessive-love" + }, + { + "label": "Older Love Interests", + "value": "older-love-interests" + }, + { + "label": "Omegaverse", + "value": "omegaverse" + }, + { + "label": "Orphans", + "value": "orphans" + }, + { + "label": "Otherworld fantasy", + "value": "otherworld-fantasy" + }, + { + "label": "Overpowered protagonist", + "value": "overpowered-protagonist" + }, + { + "label": "Past Plays a Big Role", + "value": "past-plays-a-big-role" + }, + { + "label": "Past Trauma", + "value": "past-trauma" + }, + { + "label": "Persistent Love Interests", + "value": "persistent-love-interests" + }, + { + "label": "police", + "value": "police" + }, + { + "label": "Poor Protagonist", + "value": "poor-protagonist" + }, + { + "label": "Possessive Characters", + "value": "possessive-characters" + }, + { + "label": "post apocalypse", + "value": "post-apocalypse" + }, + { + "label": "Post-apocalyptic", + "value": "post-apocalyptic" + }, + { + "label": "Post-apocalyptic background", + "value": "post-apocalyptic-background" + }, + { + "label": "Power Couple", + "value": "power-couple" + }, + { + "label": "Pretend Lovers", + "value": "pretend-lovers" + }, + { + "label": "Professional", + "value": "professional" + }, + { + "label": "Prosecutor", + "value": "prosecutor" + }, + { + "label": "Protagonist Falls in Love First", + "value": "protagonist-falls-in-love-first" + }, + { + "label": "Protagonist Strong from the Start", + "value": "protagonist-strong-from-the-start" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Quick transmigration", + "value": "quick-transmigration" + }, + { + "label": "R-18", + "value": "r-18" + }, + { + "label": "REBIRTH", + "value": "rebirth" + }, + { + "label": "Reconciliation", + "value": "reconciliation" + }, + { + "label": "Redemption", + "value": "redemption" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "reunion", + "value": "reunion" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Rich to Poor", + "value": "rich-to-poor" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "S*x Friends", + "value": "sx-friends" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "sci-fi elements", + "value": "sci-fi-elements" + }, + { + "label": "science fiction", + "value": "science-fiction" + }, + { + "label": "Second Chance", + "value": "second-chance" + }, + { + "label": "Secret Crush", + "value": "secret-crush" + }, + { + "label": "Secret Identity", + "value": "secret-identity" + }, + { + "label": "Secret Relationship", + "value": "secret-relationship" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "seme protagonist", + "value": "seme-protagonist" + }, + { + "label": "shonen ai", + "value": "shonen-ai" + }, + { + "label": "Short Story", + "value": "short-story" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen ai", + "value": "shounen-ai" + }, + { + "label": "Showbi", + "value": "showbi" + }, + { + "label": "showbiz", + "value": "showbiz" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Slow Romance", + "value": "slow-romance" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "soul-swapping", + "value": "soul-swapping" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Stoic Characters", + "value": "stoic-characters" + }, + { + "label": "Straight Seme", + "value": "straight-seme" + }, + { + "label": "Straight uke", + "value": "straight-uke" + }, + { + "label": "Straight- Gay", + "value": "straight-gay" + }, + { + "label": "Stubborn Protagonist", + "value": "stubborn-protagonist" + }, + { + "label": "Sugar daddy", + "value": "sugar-daddy" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "suspense", + "value": "suspense" + }, + { + "label": "System Administrator", + "value": "system-administrator" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Time Skip", + "value": "time-skip" + }, + { + "label": "Time Travel", + "value": "time-travel" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Tragic Past", + "value": "tragic-past" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Transplanted Memories", + "value": "transplanted-memories" + }, + { + "label": "Tsundere", + "value": "tsundere" + }, + { + "label": "Unconditional Love", + "value": "unconditional-love" + }, + { + "label": "Unlimited flow", + "value": "unlimited-flow" + }, + { + "label": "Unrequited Love", + "value": "unrequited-love" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "weak to strong", + "value": "weak-to-strong" + }, + { + "label": "wealthy characters", + "value": "wealthy-characters" + }, + { + "label": "Western", + "value": "western" + }, + { + "label": "work", + "value": "work" + }, + { + "label": "workplace", + "value": "workplace" + }, + { + "label": "Writers", + "value": "writers" + }, + { + "label": "Wu xia", + "value": "wu-xia" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Younger love interest", + "value": "younger-love-interest" + }, + { + "label": "Younger Love Interests", + "value": "younger-love-interests" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "⁸", + "value": "⁸" + }, + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Cthulhu", + "value": "cthulhu" + }, + { + "label": "Double AA", + "value": "double-aa" + }, + { + "label": "historic love", + "value": "historic-love" + }, + { + "label": "Japanese Novel", + "value": "japanese-novel" + }, + { + "label": "Kō Randō (藍銅 紅)", + "value": "ko-rando-藍銅-紅" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Light Novel (CN)", + "value": "light-novel-cn" + }, + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Original Novel", + "value": "original-novel" + }, + { + "label": "Published Novel", + "value": "published-novel" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Quick Transmigration", + "value": "quick-transmigration" + }, + { + "label": "Remove term: Chinese Novel Chinese NovelRemove term: Web Novel Web Novel", + "value": "remove-term-chinese-novel-chinese-novelremove-term-web-novel-web-novel" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "Short Story", + "value": "short-story" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/kodekslibrary.json b/plugins/multisrc/lightnovelwp/filters/kodekslibrary.json new file mode 100644 index 000000000..88eaa2598 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/kodekslibrary.json @@ -0,0 +1,200 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Kategori", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Doğa Üstü", + "value": "doga-ustu" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Geriye Dönüş", + "value": "geriye-donus" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Kara Büyü", + "value": "kara-buyu" + }, + { + "label": "Korku", + "value": "korku" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Okul", + "value": "okul" + }, + { + "label": "Psikolojik", + "value": "psikolojik" + }, + { + "label": "Romantizm", + "value": "romantizm" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Trajedi", + "value": "trajedi" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tür", + "value": [], + "options": [ + { + "label": "Akademi", + "value": "akademi" + }, + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Dahi", + "value": "dahi" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Element", + "value": "element" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Geriye Dönüş", + "value": "geriye-donus" + }, + { + "label": "Güçlü Ana Karakter", + "value": "guclu-ana-karakter" + }, + { + "label": "Kılıç Ustası", + "value": "kilic-ustasi" + }, + { + "label": "Savaşçı", + "value": "savasci" + }, + { + "label": "Yaratıklar", + "value": "yaratiklar" + }, + { + "label": "Zaman Döngüsü", + "value": "zaman-dongusu" + } + ] + }, + "status": { + "type": "Picker", + "label": "Durum", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Sırala", + "value": "", + "options": [ + { + "label": "Temizle", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Son Güncellenenler", + "value": "update" + }, + { + "label": "Yeni Eklenenler", + "value": "latest" + }, + { + "label": "Popüler", + "value": "popular" + }, + { + "label": "Puan", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/kolnovel.json b/plugins/multisrc/lightnovelwp/filters/kolnovel.json new file mode 100644 index 000000000..80bf668ad --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/kolnovel.json @@ -0,0 +1,524 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "تصنيف", + "value": [], + "options": [ + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "XUANHUAN", + "value": "xuanhuan" + }, + { + "label": "أبطال خارقين", + "value": "أبطال-خارقين" + }, + { + "label": "أساطير", + "value": "أساطير" + }, + { + "label": "أشباح", + "value": "أشباح" + }, + { + "label": "أكشن", + "value": "action" + }, + { + "label": "ألعاب", + "value": "ألعاب" + }, + { + "label": "إثارة", + "value": "excitement" + }, + { + "label": "إسلامي", + "value": "إسلامي" + }, + { + "label": "إنتقال الى عالم أخر", + "value": "isekai" + }, + { + "label": "إيتشي", + "value": "etchi" + }, + { + "label": "اكاديمي", + "value": "اكاديمي" + }, + { + "label": "اكشن", + "value": "اكشن" + }, + { + "label": "الإثارة", + "value": "الإثارة" + }, + { + "label": "الخيال العلمي", + "value": "sci-fi" + }, + { + "label": "الدراما", + "value": "الدراما" + }, + { + "label": "المغامرات", + "value": "المغامرات" + }, + { + "label": "انتقام", + "value": "انتقام" + }, + { + "label": "بطل مضاد", + "value": "بطل-مضاد" + }, + { + "label": "بطل ناضج", + "value": "بطل-ناضج" + }, + { + "label": "بقاء", + "value": "بقاء" + }, + { + "label": "بناء مملكة", + "value": "بناء-مملكة" + }, + { + "label": "بوليسي", + "value": "policy" + }, + { + "label": "تاريخ", + "value": "تاريخ" + }, + { + "label": "تاريخي", + "value": "historical" + }, + { + "label": "تحقيقات", + "value": "تحقيق" + }, + { + "label": "تشويق", + "value": "تشويق" + }, + { + "label": "تقمص شخصيات", + "value": "rpg" + }, + { + "label": "تلاعب", + "value": "تلاعب" + }, + { + "label": "تناسخ", + "value": "تناسخ" + }, + { + "label": "جريمة", + "value": "crime" + }, + { + "label": "جوسى", + "value": "josei" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "حريم", + "value": "harem" + }, + { + "label": "حل الألغاز", + "value": "حل-الألغاز" + }, + { + "label": "حياة مدرسية", + "value": "school-life" + }, + { + "label": "خارق للطبيعة", + "value": "خارق-للطبيعة" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "خيالي", + "value": "خيالي" + }, + { + "label": "خيالي(فانتازيا)", + "value": "fantasy" + }, + { + "label": "دراما", + "value": "drama" + }, + { + "label": "درامي", + "value": "درامي" + }, + { + "label": "رعب", + "value": "horror" + }, + { + "label": "رعب كوني", + "value": "رعب-كوني" + }, + { + "label": "رعب نفسي", + "value": "رعب-نفسي" + }, + { + "label": "رومانسي", + "value": "romantic" + }, + { + "label": "رومانسية", + "value": "رومانسية" + }, + { + "label": "رومنسية", + "value": "رومنسية" + }, + { + "label": "زنزانة", + "value": "زنزانة" + }, + { + "label": "زيانشيا", + "value": "زيانشيا" + }, + { + "label": "ستيم بانك", + "value": "ستيم-بانك" + }, + { + "label": "سحر", + "value": "magic" + }, + { + "label": "سفر بالزمن", + "value": "سفر-بالزمن" + }, + { + "label": "سفر عبر الزمن", + "value": "سفر-عبر-الزمن" + }, + { + "label": "سياسة", + "value": "سياسة" + }, + { + "label": "سينن", + "value": "senen" + }, + { + "label": "شريحة من الحياة", + "value": "slice-of-life" + }, + { + "label": "شعر", + "value": "شعر" + }, + { + "label": "شوانهان", + "value": "شوانهان" + }, + { + "label": "شوانهوان", + "value": "شوانهوان" + }, + { + "label": "شوجو", + "value": "shojo" + }, + { + "label": "شونين", + "value": "shonen" + }, + { + "label": "شيانشيا", + "value": "شيانشيا" + }, + { + "label": "طبي", + "value": "medical" + }, + { + "label": "ظواهر خارقة للطبيعة", + "value": "supernatural" + }, + { + "label": "عائلي", + "value": "عائلي" + }, + { + "label": "عموض", + "value": "عموض" + }, + { + "label": "غموض", + "value": "mysteries" + }, + { + "label": "فانتازي", + "value": "فانتازي" + }, + { + "label": "فانتازيا", + "value": "فانتازيا" + }, + { + "label": "فانفيك", + "value": "فانفيك" + }, + { + "label": "فنتازيا", + "value": "فنتازيا" + }, + { + "label": "فنون القتال", + "value": "martial-arts" + }, + { + "label": "فنون قتال", + "value": "فنون-قتال" + }, + { + "label": "قصة قصيرة", + "value": "قصة-قصيرة" + }, + { + "label": "قوة خارقة", + "value": "قوة-خارقة" + }, + { + "label": "قوى خارقة", + "value": "superpower" + }, + { + "label": "كوميدي", + "value": "comedy" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "كوميدية", + "value": "كوميدية" + }, + { + "label": "مأسأة", + "value": "مأسأة" + }, + { + "label": "مأساة", + "value": "مأساة" + }, + { + "label": "مأساوي", + "value": "tragedy" + }, + { + "label": "مؤامرة", + "value": "مؤامرة" + }, + { + "label": "ما بعد الكارثة", + "value": "after-the-disaster" + }, + { + "label": "ما بعد نهاية العالم", + "value": "ما-بعد-نهاية-العالم" + }, + { + "label": "مضاد البطل", + "value": "مضاد-البطل" + }, + { + "label": "مغامرات", + "value": "مغامرات" + }, + { + "label": "مغامرة", + "value": "adventure" + }, + { + "label": "ميكا", + "value": "mechanical" + }, + { + "label": "ناضج", + "value": "mature" + }, + { + "label": "نظام", + "value": "نظام" + }, + { + "label": "نفسي", + "value": "psychological" + }, + { + "label": "ون شوت", + "value": "ون-شوت" + }, + { + "label": "ووشيا", + "value": "ووشيا" + }, + { + "label": "ووكسيا", + "value": "ووكسيا" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "إنجليزية", + "value": "english" + }, + { + "label": "ترجمة إحترافية", + "value": "ترجمة-إحترافية" + }, + { + "label": "رواية لايت", + "value": "light-novel" + }, + { + "label": "رواية مؤلفة", + "value": "رواية-مؤلفة" + }, + { + "label": "رواية مترجمة", + "value": "رواية-مترجمة" + }, + { + "label": "رواية ويب", + "value": "web-novel" + }, + { + "label": "صينية", + "value": "chinese" + }, + { + "label": "عربية", + "value": "arabic" + }, + { + "label": "كورية", + "value": "korean" + }, + { + "label": "مؤلفة", + "value": "مؤلفة" + }, + { + "label": "ون شوت", + "value": "ون-شوت" + }, + { + "label": "يابانية", + "value": "japanese" + } + ] + }, + "status": { + "type": "Picker", + "label": "الحالة", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "ترتيب حسب", + "value": "", + "options": [ + { + "label": "الإعداد الأولي", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "أخر التحديثات", + "value": "update" + }, + { + "label": "أخر ما تم إضافته", + "value": "latest" + }, + { + "label": "الرائجة", + "value": "popular" + }, + { + "label": "التقييم", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/lazygirltranslations.json b/plugins/multisrc/lightnovelwp/filters/lazygirltranslations.json new file mode 100644 index 000000000..7f70280cb --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/lazygirltranslations.json @@ -0,0 +1,200 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/lightnovelbrasil.json b/plugins/multisrc/lightnovelwp/filters/lightnovelbrasil.json new file mode 100644 index 000000000..d0633352d --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/lightnovelbrasil.json @@ -0,0 +1,244 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "+18", + "value": "18" + }, + { + "label": "Ação", + "value": "acao" + }, + { + "label": "Artes Marciais", + "value": "artes-marciais" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Comédia", + "value": "comedia" + }, + { + "label": "Cultivo", + "value": "cultivo" + }, + { + "label": "Cyberpunk", + "value": "cyberpunk" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Esportes", + "value": "esportes" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Ficção Científica", + "value": "ficcao-cientifica" + }, + { + "label": "Games", + "value": "games" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Magia", + "value": "magia" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Militar", + "value": "militar" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Novel Nacional", + "value": "novel-nacional" + }, + { + "label": "Psicológico", + "value": "psicologico" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen BL", + "value": "shounen-bl" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sobrenatural", + "value": "sobrenatural" + }, + { + "label": "Terror", + "value": "terror" + }, + { + "label": "Tragédia", + "value": "tragedia" + }, + { + "label": "Vida Escolar", + "value": "vida-escolar" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tipo", + "value": [], + "options": [ + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Livro", + "value": "livro" + }, + { + "label": "One-Shot", + "value": "one-shot" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "Tudo", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Ordenar por", + "value": "", + "options": [ + { + "label": "Padrão", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Últimos Lançamentos", + "value": "update" + }, + { + "label": "Última Adição", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/lightnovelfr.json b/plugins/multisrc/lightnovelwp/filters/lightnovelfr.json new file mode 100644 index 000000000..0433034e2 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/lightnovelfr.json @@ -0,0 +1,248 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "action", + "value": "action" + }, + { + "label": "alchimie", + "value": "alchimie" + }, + { + "label": "Arts martiaux", + "value": "arts-martiaux" + }, + { + "label": "Aventure", + "value": "aventure" + }, + { + "label": "Célébrités", + "value": "celebrites" + }, + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "Combat", + "value": "combat" + }, + { + "label": "comedie", + "value": "comedie" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "dark fantasy", + "value": "dark-fantasy" + }, + { + "label": "Demon", + "value": "demon" + }, + { + "label": "Drame", + "value": "drame" + }, + { + "label": "Fantasie", + "value": "fantasie" + }, + { + "label": "Fantastique", + "value": "fantastique" + }, + { + "label": "Guildes", + "value": "guildes" + }, + { + "label": "harem", + "value": "harem" + }, + { + "label": "Idole", + "value": "idole" + }, + { + "label": "japan", + "value": "japan" + }, + { + "label": "jeux vidéos", + "value": "jeux-videos" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "korean", + "value": "korean" + }, + { + "label": "light Novel", + "value": "light-novel" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "mature", + "value": "mature" + }, + { + "label": "Mystère", + "value": "mystere" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "Renaissance", + "value": "renaissance" + }, + { + "label": "Roman", + "value": "roman" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "School life", + "value": "school-life" + }, + { + "label": "Science-fiction", + "value": "science-fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shonen", + "value": "shonen" + }, + { + "label": "showbiz", + "value": "showbiz" + }, + { + "label": "Surnaturel", + "value": "surnaturel" + }, + { + "label": "système", + "value": "systeme" + }, + { + "label": "Système de niveau", + "value": "systeme-de-niveau" + }, + { + "label": "tranche de vie", + "value": "tranche-de-vie" + }, + { + "label": "webnovel", + "value": "webnovel" + }, + { + "label": "xp", + "value": "xp" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Light novel", + "value": "light-novel" + }, + { + "label": "novel chinois", + "value": "novel-chinois" + } + ] + }, + "status": { + "type": "Picker", + "label": "Statut", + "value": "", + "options": [ + { + "label": "Tout", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Par ordre", + "value": "", + "options": [ + { + "label": "Par défaut", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Dernier mise à jour", + "value": "update" + }, + { + "label": "Derniers Ajouts", + "value": "latest" + }, + { + "label": "Populaire", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/moonlightnovel.json b/plugins/multisrc/lightnovelwp/filters/moonlightnovel.json new file mode 100644 index 000000000..9b02a161a --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/moonlightnovel.json @@ -0,0 +1,300 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Childcare", + "value": "childcare" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "Entertainment", + "value": "entertainment" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Idol", + "value": "idol" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Kindergarten", + "value": "kindergarten" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Magic Swordman", + "value": "magic-swordman" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Omegaverse", + "value": "omegaverse" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Villainess", + "value": "villainess" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "Western Style", + "value": "western-style" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Indonesian", + "value": "indonesian" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Original", + "value": "original" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/namevt.json b/plugins/multisrc/lightnovelwp/filters/namevt.json new file mode 100644 index 000000000..80d84e493 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/namevt.json @@ -0,0 +1,168 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Türler", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilimkurgu", + "value": "bilimkurgu" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hayattan Kesitler", + "value": "hayattan-kesitler" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Okul hayatı", + "value": "okul-hayati" + }, + { + "label": "Psikolojik", + "value": "psikolojik" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Tarihi", + "value": "tarihi" + }, + { + "label": "Trajedi", + "value": "trajedi" + }, + { + "label": "Yetişkin", + "value": "yetiskin" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Yazım Şekli", + "value": [], + "options": [ + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Durum", + "value": "", + "options": [ + { + "label": "Tümü", + "value": "" + }, + { + "label": "Devam Ediyor", + "value": "ongoing" + }, + { + "label": "Ara Verildi", + "value": "hiatus" + }, + { + "label": "Tamamlandı", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Sıralama", + "value": "", + "options": [ + { + "label": "Varsayılan", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Son Güncellemeler", + "value": "update" + }, + { + "label": "Son Eklenenler", + "value": "latest" + }, + { + "label": "Popüler", + "value": "popular" + }, + { + "label": "Puanlama", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/noblemtl.json b/plugins/multisrc/lightnovelwp/filters/noblemtl.json new file mode 100644 index 000000000..35034bdc2 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/noblemtl.json @@ -0,0 +1,492 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "A.I", + "value": "a-i" + }, + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Alternative History", + "value": "alternative-history" + }, + { + "label": "Another World", + "value": "another-world" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Bromance", + "value": "bromance" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cthulhu", + "value": "cthulhu" + }, + { + "label": "Dark fantasy", + "value": "dark-fantasy" + }, + { + "label": "Demons", + "value": "demons" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dystopia", + "value": "dystopia" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Entertainment", + "value": "entertainment" + }, + { + "label": "Exhaustion", + "value": "exhaustion" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "fantasy", + "value": "fantasy" + }, + { + "label": "finance", + "value": "finance" + }, + { + "label": "For men", + "value": "for-men" + }, + { + "label": "Full color", + "value": "full-color" + }, + { + "label": "fusion", + "value": "fusion" + }, + { + "label": "gacha", + "value": "gacha" + }, + { + "label": "Gallery", + "value": "gallery" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Genius", + "value": "genius" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Healing", + "value": "healing" + }, + { + "label": "Hero", + "value": "hero" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "korean novel", + "value": "korean-novel" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "List Adventure Manga Genres", + "value": "list-adventure-manga-genres" + }, + { + "label": "Long Strip", + "value": "long-strip" + }, + { + "label": "Love comedy", + "value": "love-comedy" + }, + { + "label": "magic", + "value": "magic" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Middle Ages", + "value": "middle-ages" + }, + { + "label": "Misunderstanding", + "value": "misunderstanding" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "modern fantasy", + "value": "modern-fantasy" + }, + { + "label": "Munchkin", + "value": "munchkin" + }, + { + "label": "music", + "value": "music" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Necromancy", + "value": "necromancy" + }, + { + "label": "No Romance", + "value": "no-romance" + }, + { + "label": "NTL", + "value": "ntl" + }, + { + "label": "o", + "value": "o" + }, + { + "label": "Obsession", + "value": "obsession" + }, + { + "label": "Politics", + "value": "politics" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Programming", + "value": "programming" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Pure Love", + "value": "pure-love" + }, + { + "label": "reasoning", + "value": "reasoning" + }, + { + "label": "Redemption", + "value": "redemption" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Regret", + "value": "regret" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Return", + "value": "return" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Reversal", + "value": "reversal" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romance Fanrasy", + "value": "romance-fanrasy" + }, + { + "label": "Salvation", + "value": "salvation" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Science fiction", + "value": "science-fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Soft yandere", + "value": "soft-yandere" + }, + { + "label": "Space opera", + "value": "space-opera" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "system", + "value": "system" + }, + { + "label": "Time limit", + "value": "time-limit" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "TRPG", + "value": "trpg" + }, + { + "label": "TS", + "value": "ts" + }, + { + "label": "Tsundere", + "value": "tsundere" + }, + { + "label": "Unique", + "value": "unique" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Villain", + "value": "villain" + }, + { + "label": "Wholesome", + "value": "wholesome" + }, + { + "label": "Wisdom", + "value": "wisdom" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yandere", + "value": "yandere" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese novel", + "value": "chinese-novel" + }, + { + "label": "habyeol", + "value": "habyeol" + }, + { + "label": "korean novel", + "value": "korean-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "삼심", + "value": "삼심" + }, + { + "label": "호곡", + "value": "호곡" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/novelsknight.json b/plugins/multisrc/lightnovelwp/filters/novelsknight.json new file mode 100644 index 000000000..31359f704 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/novelsknight.json @@ -0,0 +1,368 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "city", + "value": "city" + }, + { + "label": "cultivation", + "value": "cultivation" + }, + { + "label": "faloo", + "value": "faloo" + }, + { + "label": "Fan fiction", + "value": "fan-fiction" + }, + { + "label": "fanfiction", + "value": "fanfiction" + }, + { + "label": "fanqie", + "value": "fanqie" + }, + { + "label": "fanstay", + "value": "fanstay" + }, + { + "label": "fantansy", + "value": "fantansy" + }, + { + "label": "fantasy", + "value": "fantasy" + }, + { + "label": "horror", + "value": "horror" + }, + { + "label": "immortal", + "value": "immortal" + }, + { + "label": "Infinite Heavens", + "value": "infinite-heavens" + }, + { + "label": "Infinity", + "value": "infinity" + }, + { + "label": "light novel", + "value": "light-novel" + }, + { + "label": "martial arts", + "value": "martial-arts" + }, + { + "label": "military history", + "value": "military-history" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "qidian", + "value": "qidian" + }, + { + "label": "rebirth", + "value": "rebirth" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "sc-fi online game", + "value": "sc-fi-online-game" + }, + { + "label": "Sci-fi online games", + "value": "sci-fi-online-games" + }, + { + "label": "Science fiction online game", + "value": "science-fiction-online-game" + }, + { + "label": "sport", + "value": "sport" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "system", + "value": "system" + }, + { + "label": "time travel", + "value": "time-travel" + }, + { + "label": "travel", + "value": "travel" + }, + { + "label": "Unlimited Heavens", + "value": "unlimited-heavens" + }, + { + "label": "urban", + "value": "urban" + }, + { + "label": "urrban", + "value": "urrban" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Black Basket: Platinum Star", + "value": "black-basket-platinum-star" + }, + { + "label": "chinesse", + "value": "chinesse" + }, + { + "label": "ciweimao", + "value": "ciweimao" + }, + { + "label": "Doomsday", + "value": "doomsday" + }, + { + "label": "dragon ball", + "value": "dragon-ball" + }, + { + "label": "Entertainment", + "value": "entertainment" + }, + { + "label": "f", + "value": "f" + }, + { + "label": "falo", + "value": "falo" + }, + { + "label": "faloo", + "value": "faloo" + }, + { + "label": "falooo", + "value": "falooo" + }, + { + "label": "fan fiction", + "value": "fan-fiction" + }, + { + "label": "fanqie", + "value": "fanqie" + }, + { + "label": "fanqienovel", + "value": "fanqienovel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "faoo", + "value": "faoo" + }, + { + "label": "Football", + "value": "football" + }, + { + "label": "Harry Potter", + "value": "harry-potter" + }, + { + "label": "horror", + "value": "horror" + }, + { + "label": "Hunter X Hunter", + "value": "hunter-x-hunter" + }, + { + "label": "Infinite Evolution", + "value": "infinite-evolution" + }, + { + "label": "jjwxc", + "value": "jjwxc" + }, + { + "label": "Lords: Opens with a Bloody Nest", + "value": "lords-opens-with-a-bloody-nest" + }, + { + "label": "martial arts", + "value": "martial-arts" + }, + { + "label": "marvel", + "value": "marvel" + }, + { + "label": "naruto", + "value": "naruto" + }, + { + "label": "nba", + "value": "nba" + }, + { + "label": "nove", + "value": "nove" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "one piece", + "value": "one-piece" + }, + { + "label": "online game", + "value": "online-game" + }, + { + "label": "Please", + "value": "please" + }, + { + "label": "qiamao", + "value": "qiamao" + }, + { + "label": "qidian", + "value": "qidian" + }, + { + "label": "qimao", + "value": "qimao" + }, + { + "label": "qq", + "value": "qq" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Science-fiction", + "value": "science-fiction" + }, + { + "label": "sfacg", + "value": "sfacg" + }, + { + "label": "this is a world of horror", + "value": "this-is-a-world-of-horror" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/novelsparadise.json b/plugins/multisrc/lightnovelwp/filters/novelsparadise.json new file mode 100644 index 000000000..78849a3dd --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/novelsparadise.json @@ -0,0 +1,220 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "+18", + "value": "18" + }, + { + "label": "آليه", + "value": "آليه" + }, + { + "label": "اكشن", + "value": "اكشن" + }, + { + "label": "بالغ", + "value": "بالغ" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حياة مدرسية", + "value": "حياة-مدرسية" + }, + { + "label": "خارق للطبيعة", + "value": "خارق-للطبيعة" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رومانسي", + "value": "رومانسي" + }, + { + "label": "سحر", + "value": "سحر" + }, + { + "label": "سنين", + "value": "سنين" + }, + { + "label": "شبه بشريه", + "value": "شبه-بشريه" + }, + { + "label": "شرقي", + "value": "شرقي" + }, + { + "label": "شريحة من الحياه", + "value": "شريحة-من-الحياه" + }, + { + "label": "شوانهوان", + "value": "شوانهوان" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "شيانشيا", + "value": "شيانشيا" + }, + { + "label": "صيني", + "value": "صيني" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فان فيكشن", + "value": "فان-فيكشن" + }, + { + "label": "فنون قتاليه", + "value": "فنون-قتاليه" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "مأساوي", + "value": "مأساوي" + }, + { + "label": "مغام", + "value": "مغام" + }, + { + "label": "مغامره", + "value": "مغامره" + }, + { + "label": "نفسي", + "value": "نفسي" + }, + { + "label": "ووشيا", + "value": "ووشيا" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "النوع", + "value": [], + "options": [ + { + "label": "روايات انجليزية", + "value": "روايات-انجليزية" + }, + { + "label": "روايات صينيه", + "value": "روايات-صينيه" + }, + { + "label": "روايات كوريه", + "value": "روايات-كوريه" + }, + { + "label": "روايات ويب", + "value": "روايات-ويب" + }, + { + "label": "روايات يابانية", + "value": "روايات-يابانية" + } + ] + }, + "status": { + "type": "Picker", + "label": "الحالة", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "ترتيب بواسطة", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "أخر التحديثات", + "value": "update" + }, + { + "label": "المضاف حديثاً", + "value": "latest" + }, + { + "label": "شائع", + "value": "popular" + }, + { + "label": "{{advanced_search_series_order_by_rating_label}}", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/noveltr.json b/plugins/multisrc/lightnovelwp/filters/noveltr.json new file mode 100644 index 000000000..f91330f76 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/noveltr.json @@ -0,0 +1,252 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "dövüş sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "ecchi", + "value": "ecchi" + }, + { + "label": "fantastik", + "value": "fantastik" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "korku", + "value": "korku" + }, + { + "label": "macera", + "value": "macera" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "okul", + "value": "okul" + }, + { + "label": "oyun", + "value": "oyun" + }, + { + "label": "psikoloji", + "value": "psikoloji" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "süper kahraman", + "value": "super-kahraman" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "tarih", + "value": "tarih" + }, + { + "label": "trajedi", + "value": "trajedi" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "yetişkin", + "value": "yetiskin" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Tür", + "value": [], + "options": [ + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Durum", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "Devam Ediyor", + "value": "ongoing" + }, + { + "label": "Askıda", + "value": "hiatus" + }, + { + "label": "Tamamlanmış", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Sıralama", + "value": "", + "options": [ + { + "label": "Varsayılan", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/pandamtl.json b/plugins/multisrc/lightnovelwp/filters/pandamtl.json new file mode 100644 index 000000000..961103b9a --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/pandamtl.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "High Water Level", + "value": "high-water-level" + }, + { + "label": "Hypnosis", + "value": "hypnosis" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Masculine", + "value": "masculine" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Munchkin", + "value": "munchkin" + }, + { + "label": "NTL", + "value": "ntl" + }, + { + "label": "Orgy", + "value": "orgy" + }, + { + "label": "Pure Love", + "value": "pure-love" + }, + { + "label": "Rice Cake", + "value": "rice-cake" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoonization", + "value": "webtoonization" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "we", + "value": "we" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "원룸 패밀리", + "value": "원룸-패밀리" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/requiemtls.json b/plugins/multisrc/lightnovelwp/filters/requiemtls.json new file mode 100644 index 000000000..71c919a81 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/requiemtls.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Daily Life", + "value": "daily-life" + }, + { + "label": "Distantchicken", + "value": "distantchicken" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gallery", + "value": "gallery" + }, + { + "label": "Growth", + "value": "growth" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hero", + "value": "hero" + }, + { + "label": "Internet Broadcasting", + "value": "internet-broadcasting" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Munchkin", + "value": "munchkin" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Obsession", + "value": "obsession" + }, + { + "label": "Possession", + "value": "possession" + }, + { + "label": "Pure Love", + "value": "pure-love" + }, + { + "label": "Purelove", + "value": "purelove" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Regret", + "value": "regret" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Salvation", + "value": "salvation" + }, + { + "label": "SF", + "value": "sf" + }, + { + "label": "SM", + "value": "sm" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "TS", + "value": "ts" + }, + { + "label": "Yandere", + "value": "yandere" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Mature", + "value": "mature" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/sektenovel.json b/plugins/multisrc/lightnovelwp/filters/sektenovel.json new file mode 100644 index 000000000..b4a2ad867 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/sektenovel.json @@ -0,0 +1,152 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Light Novel (JP)", + "value": "light-novel-jp" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/systemtranslation.json b/plugins/multisrc/lightnovelwp/filters/systemtranslation.json new file mode 100644 index 000000000..5ab8ea2cf --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/systemtranslation.json @@ -0,0 +1,184 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Anime & Comics", + "value": "anime-comics" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Translated Novel", + "value": "translated-novel" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Webnovel (FF)", + "value": "webnovel-ff" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/transweaver.json b/plugins/multisrc/lightnovelwp/filters/transweaver.json new file mode 100644 index 000000000..380ff3d8c --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/transweaver.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "idol", + "value": "idol" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Subscription", + "value": "subscription" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Light Novel (KR)", + "value": "light-novel-kr" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/universalnovel.json b/plugins/multisrc/lightnovelwp/filters/universalnovel.json new file mode 100644 index 000000000..e89ac0434 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/universalnovel.json @@ -0,0 +1,272 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Acting", + "value": "acting" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Bl", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "entertainment", + "value": "entertainment" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Funny", + "value": "funny" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Ghost", + "value": "ghost" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Jose", + "value": "jose" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Male protagonist", + "value": "male-protagonist" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sprited animal", + "value": "sprited-animal" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Unlimited flow", + "value": "unlimited-flow" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Published Novel (CN)", + "value": "published-novel-cn" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/vandytranslate.json b/plugins/multisrc/lightnovelwp/filters/vandytranslate.json new file mode 100644 index 000000000..eb4e3fedf --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/vandytranslate.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Medical", + "value": "medical" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [ + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Published Novel (KR)", + "value": "published-novel-kr" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/filters/whitemoonlightnovels.json b/plugins/multisrc/lightnovelwp/filters/whitemoonlightnovels.json new file mode 100644 index 000000000..4d6049980 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/filters/whitemoonlightnovels.json @@ -0,0 +1,163 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Boy's Love", + "value": "boys-love" + }, + { + "label": "Business", + "value": "business" + }, + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Dropped", + "value": "dropped" + }, + { + "label": "Entertainment Industry", + "value": "entertainment-industry" + }, + { + "label": "Gaming", + "value": "gaming" + }, + { + "label": "Ger", + "value": "ger" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Omegaverse", + "value": "omegaverse" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Unlimited Flow", + "value": "unlimited-flow" + }, + { + "label": "Variety Show", + "value": "variety-show" + } + ] + }, + "type[]": { + "type": "Checkbox", + "label": "Type", + "value": [], + "options": [] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "Ongoing", + "value": "ongoing" + }, + { + "label": "Hiatus", + "value": "hiatus" + }, + { + "label": "Completed", + "value": "completed" + } + ] + }, + "order": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Default", + "value": "" + }, + { + "label": "A-Z", + "value": "title" + }, + { + "label": "Z-A", + "value": "titlereverse" + }, + { + "label": "Latest Update", + "value": "update" + }, + { + "label": "Latest Added", + "value": "latest" + }, + { + "label": "Popular", + "value": "popular" + }, + { + "label": "Rating", + "value": "rating" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/lightnovelwp/generator.js b/plugins/multisrc/lightnovelwp/generator.js new file mode 100644 index 000000000..d6bcd242e --- /dev/null +++ b/plugins/multisrc/lightnovelwp/generator.js @@ -0,0 +1,43 @@ +import list from './sources.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(source => { + const exist = existsSync(join(folder, 'filters', source.id + '.json')); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.id + '.json'), + ); + source.filters = JSON.parse(filters).filters; + } + console.log( + `[lightnovelwp] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const LightNovelWPTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${LightNovelWPTemplate.replace( + '// CustomJS HERE', + source.options?.customJs || '', +)} +const plugin = new LightNovelWPPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: source.options?.lang || 'English', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/lightnovelwp/get_filters.js b/plugins/multisrc/lightnovelwp/get_filters.js new file mode 100644 index 000000000..732533618 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/get_filters.js @@ -0,0 +1,167 @@ +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import * as readline from 'readline'; +import process from 'process'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function getFilters(name, html) { + const $ = cheerio.load(html); + const filters = { + filters: { + 'genre[]': { + type: 'Checkbox', + label: 'Genre', + value: [], + options: [], + }, + 'type[]': { + type: 'Checkbox', + label: 'Type', + value: [], + options: [], + }, + 'status': { + type: 'Picker', + label: 'Status', + value: '', + options: [], + }, + 'order': { + type: 'Picker', + label: 'Order', + value: '', + options: [], + }, + }, + }; + + const filtersContainer = $('div.quickfilter').find('ul'); + filtersContainer.each((i, el) => { + const filterName = Object.keys(filters.filters)[i]; + if (filterName) { + filters.filters[filterName].label = $(el) + .prev() + .contents() + .first() + .text() + .trim(); + $(el) + .find('li') + .each((j, li) => { + filters.filters[filterName].options.push({ + label: $(li).text().trim(), + value: decodeURI($(li).find('input').attr('value') || ''), + }); + }); + } + }); + + if ( + filters.filters['genre[]'].options.length == 0 || + filters.filters['type[]'].options.length == 0 || + filters.filters['status'].options.length == 0 || + filters.filters['order'].options.length == 0 + ) { + console.error( + `🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + '.json')})🚨`, + ); + } + + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + JSON.stringify(filters, null, 2), + ); + console.log(`✅Filters created successfully for ${name}✅`); +} + +async function getFiltersFromURL(name, url) { + const response = await fetch(url + '/series/'); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}, while fetching ${response.url}`, + ); + } + const html = await response.text(); + try { + getFilters(name, html); + } catch (e) { + console.error('Error while getting filters from', url); + console.error('(' + e + ')'); + } +} + +async function askGetFilter() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await rl.question( + 'Enter the id of the site (same one as in sources.json): ', + async name => { + await rl.question( + EREASE_PREV_LINE + + "Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ", + async method => { + if (method.toLowerCase() === 'url') { + const sources = JSON.parse( + fs.readFileSync(path.join(__dirname, 'sources.json'), 'utf-8'), + ); + const source = sources.find(s => s.id === name); + if (source && source.sourceSite) { + console.log('Getting filters from', source.sourceSite); + try { + await getFiltersFromURL(name, source.sourceSite); + } catch (e) { + console.error( + 'Error while getting filters from', + source.sourceSite, + ); + console.log(e.message || e); + } + rl.close(); + } else { + await rl.question( + EREASE_PREV_LINE + + 'Enter the URL (same one as in sources.json): ', + async url => { + rl.close(); + try { + await getFiltersFromURL(name, url); + } catch (e) { + console.error('Error while getting filters from', url); + console.log(e.message || e); + } + }, + ); + } + } else { + process.stdout.write( + EREASE_PREV_LINE + + `Enter the html text from the page at {sourceSite}/series (at the end press ENTER then press CTRL+C) +(to make it faster you can run \`$("div.quickfilter").parent().html()\` in the console to get only the important html part): `, + ); + let html = ''; + rl.on('SIGINT', () => { + console.log('Stopeed reading input, creating filters file'); + getFilters(name, html); + rl.close(); + }); + rl.on('line', line => { + html += line + '\n'; + }); + } + }, + ); + }, + ); +} + +askGetFilter(); + +export { getFiltersFromURL }; diff --git a/plugins/multisrc/lightnovelwp/sources.json b/plugins/multisrc/lightnovelwp/sources.json new file mode 100644 index 000000000..c83dd10dd --- /dev/null +++ b/plugins/multisrc/lightnovelwp/sources.json @@ -0,0 +1,321 @@ +[ + { + "id": "lightnovelfr", + "sourceSite": "https://lightnovelfr.com/", + "sourceName": "Ligh Novel FR", + "options": { + "lang": "French", + "reverseChapters": true + } + }, + { + "id": "kolnovel", + "sourceSite": "https://kolnovel.com/", + "sourceName": "Kol Novel", + "options": { + "lang": "Arabic", + "reverseChapters": true, + "customJs": "$('article > style').text().match(/\\.\\w+(?=\\s*[,{])/g)?.forEach(tag => $(`p${tag}`).remove());$('.epcontent .code-block').remove();", + "versionIncrements": 10 + } + }, + { + "id": "freekolnovel", + "sourceSite": "https://free.kolnovel.com/", + "sourceName": "Free Kol Novel", + "options": { + "lang": "Arabic", + "reverseChapters": true, + "customJs": "$('article > style').text().match(/\\.\\w+(?=\\s*[,{])/g)?.forEach(tag => $(`p${tag}`).remove());$('.epcontent .code-block').remove();" + } + }, + { + "id": "knoxt", + "sourceSite": "https://knoxt.space/", + "sourceName": "KnoxT", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "bacalightnovel", + "sourceSite": "https://bacalightnovel.co/", + "sourceName": "Baca Light Novel", + "options": { + "lang": "Indonesian", + "reverseChapters": true + } + }, + { + "id": "allnovelread", + "sourceSite": "https://allnovelread.com/", + "sourceName": "AllNovelRead", + "options": { + "lang": "Spanish", + "reverseChapters": true, + "down": true, + "downSince": 1768289212954 + } + }, + { + "id": "daotranslate", + "sourceSite": "https://daotranslate.com/", + "sourceName": "DaoTranslate", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "novelsparadise", + "sourceSite": "https://novelsparadise.site/", + "sourceName": "Novels Paradise", + "options": { + "lang": "Arabic", + "reverseChapters": true + } + }, + { + "id": "universalnovel", + "sourceSite": "https://universalnovel.com/", + "sourceName": "Universal Novel", + "options": { + "lang": "English", + "reverseChapters": false + } + }, + { + "id": "noveltr", + "sourceSite": "https://noveltr.com/", + "sourceName": "NovelTR", + "options": { + "lang": "Turkish" + } + }, + { + "id": "noblemtl", + "sourceSite": "https://noblemtl.com/", + "sourceName": "NobleMTL", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "pandamtl", + "sourceSite": "https://pandamtl.com/", + "sourceName": "Panda Machine Translations", + "options": { + "lang": "English", + "reverseChapters": true, + "down": true, + "downSince": 1768289212928 + } + }, + { + "id": "novelsknight", + "sourceSite": "https://novelsknight.punchmanga.online/", + "sourceName": "NovelsKnight", + "options": { + "lang": "English", + "reverseChapters": true, + "versionIncrements": 2, + "customJs": "$('.announ').remove(); return $('.epcontent').eq(-1).find('p').map(function (i, el) { return '<p>' + $(this).text() + '</p>'; }).toArray().join('\\n') || '';" + } + }, + { + "id": "centralnovel", + "sourceSite": "https://centralnovel.com/", + "sourceName": "Central Novel", + "options": { + "lang": "Portuguese", + "reverseChapters": true + } + }, + { + "id": "whitemoonlightnovels", + "sourceSite": "https://whitemoonlightnovels.com/", + "sourceName": "White Moonlight Novels", + "options": { + "lang": "English", + "reverseChapters": false + } + }, + { + "id": "sektenovel", + "sourceSite": "https://sektenovel.web.id/", + "sourceName": "Sekte Novel", + "options": { + "lang": "Indonesian", + "reverseChapters": true + } + }, + { + "id": "systemtranslation", + "sourceSite": "https://systemtranslation.com/", + "sourceName": "System Translation", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "moonlightnovel", + "sourceSite": "https://moonlightnovel.com/", + "sourceName": "Moonlight Novels", + "options": { + "versionIncrements": 1, + "lang": "English", + "reverseChapters": true, + "down": true, + "downSince": 1768289212919 + } + }, + { + "id": "lightnovelbrasil", + "sourceSite": "https://lightnovelbrasil.com/", + "sourceName": "Light Novel Brasil", + "options": { + "lang": "Portuguese", + "reverseChapters": true + } + }, + { + "id": "ellotl", + "sourceSite": "https://ellotl.com/", + "sourceName": "ElloTL", + "options": { + "lang": "English", + "reverseChapters": true, + "down": true, + "downSince": 1768289212947 + } + }, + { + "id": "ippotranslations", + "sourceSite": "https://ippotranslations.com/", + "sourceName": "Ippotranslations", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "cpunovel", + "sourceSite": "https://cpunovel.com/", + "sourceName": "CPUnovel", + "options": { + "lang": "English", + "reverseChapters": true, + "down": true, + "downSince": 1768289212904 + } + }, + { + "id": "requiemtls", + "sourceSite": "https://requiemtls.com/", + "sourceName": "Requiem Translations", + "options": { + "lang": "English", + "reverseChapters": true, + "customJs": "\n $('div.entry-content script').remove();\n\n const url = this.site + chapterPath.slice(0, -1);\n const offsets = [[0, 12368, 12462], [1, 6960, 7054], [2, 4176, 4270]];\n const idx = url.length * url.charCodeAt(url.length - 1) * 2 % 3;\n const [_, offsetLower, offsetCap] = offsets[idx] ?? offsets[0];\n\n const asciiA = 'A'.charCodeAt(0);\n const asciiz = 'z'.charCodeAt(0);\n\n $('div.entry-content > p').text((_, txt) =>\n txt.split('').map(char => {\n const code = char.charCodeAt(0);\n const offset = (code >= offsetLower + asciiA && code <= offsetLower + asciiz)\n ? offsetLower\n : offsetCap;\n const decoded = code - offset;\n return (decoded >= 32 && decoded <= 126) ? String.fromCharCode(decoded) : char;\n }).join('')\n );\n", + "versionIncrements": 4 + } + }, + { + "id": "betternovels", + "sourceSite": "https://betternovels.net/", + "sourceName": "Better Novels", + "options": { + "lang": "Portuguese", + "down": true, + "downSince": 1768289212945 + } + }, + { + "id": "arcane", + "sourceSite": "https://arcanetranslations.com/", + "sourceName": "Arcane Translations", + "options": { + "reverseChapters": true, + "lang": "English", + "hasLocked": true + } + }, + { + "id": "namevt", + "sourceSite": "https://namevt.com/", + "sourceName": "Namevt", + "options": { + "reverseChapters": true, + "seriesPath": "seri", + "lang": "Turkish" + } + }, + { + "id": "kodekslibrary", + "sourceSite": "https://www.kodekslibrary.com/", + "sourceName": "Kodeks Library", + "options": { + "reverseChapters": true, + "lang": "Turkish" + } + }, + { + "id": "vandytranslate", + "sourceSite": "https://vandytranslate.com", + "sourceName": "Vandy Translate", + "options": { + "reverseChapters": true, + "lang": "English" + } + }, + { + "id": "lazygirltranslations", + "sourceSite": "https://lazygirltranslations.com", + "sourceName": "Lazy Girl Translations", + "options": { + "reverseChapters": false, + "lang": "English" + } + }, + { + "id": "keopi", + "sourceSite": "https://keopitranslations.com", + "sourceName": "Keopi Translations", + "options": { + "lang": "English", + "reverseChapters": true, + "down": true, + "downSince": 1768289212914 + } + }, + { + "id": "TCSega", + "sourceSite": "https://teamchmantranslations.com", + "sourceName": "TC & Sega", + "options": { + "lang": "Spanish", + "reverseChapters": true + } + }, + { + "id": "transweaver", + "sourceSite": "https://transweaver.com/", + "sourceName": "Translation Weaver", + "options": { + "lang": "English", + "reverseChapters": true + } + }, + { + "id": "kdtnovels", + "sourceSite": "https://kdtnovels.net/", + "sourceName": "KDT Novels", + "options": { + "lang": "English", + "reverseChapters": true + } + } +] diff --git a/plugins/multisrc/lightnovelwp/template.ts b/plugins/multisrc/lightnovelwp/template.ts new file mode 100644 index 000000000..8234b5cf4 --- /dev/null +++ b/plugins/multisrc/lightnovelwp/template.ts @@ -0,0 +1,465 @@ +import { load } from 'cheerio'; +import { Parser } from 'htmlparser2'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; +import { storage } from '@libs/storage'; + +type LightNovelWPOptions = { + reverseChapters?: boolean; + lang?: string; + versionIncrements?: number; + seriesPath?: string; + customJs?: string; + hasLocked?: boolean; +}; + +export type LightNovelWPMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: LightNovelWPOptions; + filters?: Filters; +}; + +export class LightNovelWPPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options?: LightNovelWPOptions; + filters?: Filters; + + hideLocked = storage.get('hideLocked'); + pluginSettings?: Filters; + + constructor(metadata: LightNovelWPMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/lightnovelwp/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `1.1.${10 + versionIncrements}`; + this.options = metadata.options ?? ({} as LightNovelWPOptions); + this.filters = metadata.filters satisfies Filters; + + if (this.options?.hasLocked) { + this.pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + } + } + + getHostname(url: string): string { + url = url.split('/')[2]; + const url_parts = url.split('.'); + url_parts.pop(); // remove TLD + return url_parts.join('.'); + } + + async safeFecth(url: string, search: boolean): Promise<string> { + const urlParts = url.split('://'); + const protocol = urlParts.shift(); + const sanitizedUri = urlParts[0].replace(/\/\//g, '/'); + const r = await fetchApi(protocol + '://' + sanitizedUri); + if (!r.ok && search != true) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const data = await r.text(); + const title = data.match(/<title>(.*?)<\/title>/)?.[1]?.trim(); + + if ( + this.getHostname(url) != this.getHostname(r.url) || + (title && + (title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...')) + ) + throw new Error( + 'Captcha error, please open in webview (or the website has changed url)', + ); + + return data; + } + + parseNovels(html: string): Plugin.NovelItem[] { + html = load(html).html(); // fix "'" beeing replaced by "’" (html entities) + const novels: Plugin.NovelItem[] = []; + + const articles = html.match(/<article([^]*?)<\/article>/g) || []; + articles.forEach(article => { + const [, novelUrl, novelName] = + article.match(/<a href="([^"]*)".*? title="([^"]*)"/) || []; + + if (novelName && novelUrl) { + const novelCover = + article.match( + /<img [^>]*?src="([^"]*)"[^>]*?(?: data-src="([^"]*)")?[^>]*>/, + ) || []; + + let novelPath; + if (novelUrl.includes(this.site)) { + novelPath = novelUrl.replace(this.site, ''); + } else { + // TODO: report website new url to server + const novelParts = novelUrl.split('/'); + novelParts.shift(); + novelParts.shift(); + novelParts.shift(); + novelPath = novelParts.join('/'); + } + + novels.push({ + name: novelName, + cover: novelCover[2] || novelCover[1] || defaultCover, + path: novelPath, + }); + } + }); + + return novels; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const seriesPath = this.options?.seriesPath ?? '/series/'; + let url = this.site + seriesPath + '?page=' + pageNo; + if (!filters) filters = this.filters || {}; + if (showLatestNovels) url += '&order=latest'; + for (const key in filters) { + if (typeof filters[key].value === 'object') + for (const value of filters[key].value as string[]) + url += `&${key}=${value}`; + else if (filters[key].value) url += `&${key}=${filters[key].value}`; + } + const html = await this.safeFecth(url, false); + return this.parseNovels(html); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const baseURL = this.site; + const html = await this.safeFecth(baseURL + novelPath, false); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + genres: '', + summary: '', + author: '', + artist: '', + status: '', + chapters: [] as Plugin.ChapterItem[], + }; + let isParsingGenres = false; + let isReadingGenre = false; + let isReadingSummary = 0; + let isParsingInfo = false; + let isReadingInfo = false; + let isReadingAuthor = false; + let isReadingArtist = false; + let isReadingStatus = false; + let isParsingChapterList = false; + let isReadingChapter = false; + let isReadingChapterInfo = 0; + let isPaidChapter = false; + let hasLockItemOnChapterNum = false; + const chapters: Plugin.ChapterItem[] = []; + let tempChapter = {} as Plugin.ChapterItem; + const hideLocked = this.hideLocked; + + const parser = new Parser({ + onopentag(name, attribs) { + // name and cover + if (!novel.cover && attribs['class']?.includes('ts-post-image')) { + novel.name = attribs['title']; + novel.cover = attribs['data-src'] || attribs['src'] || defaultCover; + } // genres + else if ( + attribs['class'] === 'genxed' || + attribs['class'] === 'sertogenre' + ) { + isParsingGenres = true; + } else if (isParsingGenres && name === 'a') { + isReadingGenre = true; + } // summary + else if ( + name === 'div' && + (attribs['class'] === 'entry-content' || + attribs['itemprop'] === 'description') + ) { + isReadingSummary++; + } // author and status + else if (attribs['class'] === 'spe' || attribs['class'] === 'serl') { + isParsingInfo = true; + } else if (isParsingInfo && name === 'span') { + isReadingInfo = true; + } else if (name === 'div' && attribs['class'] === 'sertostat') { + isParsingInfo = true; + isReadingInfo = true; + isReadingStatus = true; + } + // chapters + else if (attribs['class'] && attribs['class'].includes('eplister')) { + isParsingChapterList = true; + } else if (isParsingChapterList && name === 'li') { + isReadingChapter = true; + } else if (isReadingChapter) { + if (name === 'a' && tempChapter.path === undefined) { + tempChapter.path = attribs['href'].replace(baseURL, '').trim(); + } else if (attribs['class'] === 'epl-num') { + isReadingChapterInfo = 1; + } else if (attribs['class'] === 'epl-title') { + isReadingChapterInfo = 2; + } else if (attribs['class'] === 'epl-date') { + isReadingChapterInfo = 3; + } else if (attribs['class'] === 'epl-price') { + isReadingChapterInfo = 4; + } + } else if (isReadingSummary && (name === 'div' || name === 'script')) { + isReadingSummary++; + } + }, + ontext(data) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + novel.genres += data + ', '; + } + } // summary + else if (isReadingSummary === 1 && data.trim()) { + novel.summary += data; + } // author and status + else if (isParsingInfo) { + if (isReadingInfo) { + const detailName = data.toLowerCase().replace(':', '').trim(); + + if (isReadingAuthor) { + novel.author += data || 'Unknown'; + } else if (isReadingArtist) { + novel.artist += data || 'Unknown'; + } else if (isReadingStatus) { + switch (detailName) { + case 'مكتملة': + case 'completed': + case 'complété': + case 'completo': + case 'completado': + case 'tamamlandı': + novel.status = NovelStatus.Completed; + break; + case 'مستمرة': + case 'ongoing': + case 'en cours': + case 'em andamento': + case 'en progreso': + case 'devam ediyor': + novel.status = NovelStatus.Ongoing; + break; + case 'متوقفة': + case 'hiatus': + case 'en pause': + case 'hiato': + case 'pausa': + case 'pausado': + case 'duraklatıldı': + novel.status = NovelStatus.OnHiatus; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + + switch (detailName) { + case 'الكاتب': + case 'author': + case 'auteur': + case 'autor': + case 'yazar': + isReadingAuthor = true; + break; + case 'الحالة': + case 'status': + case 'statut': + case 'estado': + case 'durum': + isReadingStatus = true; + break; + case 'الفنان': + case 'artist': + case 'artiste': + case 'artista': + case 'çizer': + isReadingArtist = true; + break; + } + } + } // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + if (isReadingChapterInfo === 1) { + if (data.includes('🔒')) { + isPaidChapter = true; + hasLockItemOnChapterNum = true; + } else if (hasLockItemOnChapterNum) { + isPaidChapter = false; + } + extractChapterNumber(data, tempChapter); + } else if (isReadingChapterInfo === 2) { + tempChapter.name = + data + .match( + RegExp( + `^${novel.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*(.+)`, + ), + )?.[1] + ?.trim() || data.trim(); + if (!tempChapter.chapterNumber) { + extractChapterNumber(data, tempChapter); + } + } else if (isReadingChapterInfo === 3) { + tempChapter.releaseTime = data; //new Date(data).toISOString(); + } else if (isReadingChapterInfo === 4) { + const detailName = data.toLowerCase().trim(); + switch (detailName) { + case 'free': + case 'gratuit': + case 'مجاني': + case 'livre': + case '': + isPaidChapter = false; + break; + default: + isPaidChapter = true; + break; + } + } + } + } + }, + onclosetag(name) { + // genres + if (isParsingGenres) { + if (isReadingGenre) { + isReadingGenre = false; // stop reading genre + } else { + isParsingGenres = false; // stop parsing genres + novel.genres = novel.genres?.slice(0, -2); // remove trailing comma + } + } // summary + else if (isReadingSummary) { + if (name === 'p') { + novel.summary += '\n\n'; + } else if (name === 'br') { + novel.summary += '\n'; + } else if (name === 'div' || name === 'script') { + isReadingSummary--; + } + } // author and status + else if (isParsingInfo) { + if (isReadingInfo) { + if (name === 'span') { + isReadingInfo = false; + if (isReadingAuthor && novel.author) { + isReadingAuthor = false; + } else if (isReadingArtist && novel.artist) { + isReadingArtist = false; + } else if (isReadingStatus && novel.status !== '') { + isReadingStatus = false; + } + } + } else if (name === 'div') { + isParsingInfo = false; + novel.author = novel.author?.trim(); + novel.artist = novel.artist?.trim(); + } + } // chapters + else if (isParsingChapterList) { + if (isReadingChapter) { + if (isReadingChapterInfo === 1) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 2) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 3) { + isReadingChapterInfo = 0; + } else if (isReadingChapterInfo === 4) { + isReadingChapterInfo = 0; + } else if (name === 'li') { + isReadingChapter = false; + if (!tempChapter.chapterNumber) tempChapter.chapterNumber = 0; + if (isPaidChapter) tempChapter.name = '🔒 ' + tempChapter.name; + if (!hideLocked || !isPaidChapter) chapters.push(tempChapter); + tempChapter = {} as Plugin.ChapterItem; + } + } else if (name === 'ul') { + isParsingChapterList = false; + } + } + }, + }); + + parser.write(html); + parser.end(); + + if (chapters.length) { + if (this.options?.reverseChapters) chapters.reverse(); + novel.chapters = chapters; + } + + novel.summary = novel.summary.trim(); + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + let data = await this.safeFecth(this.site + chapterPath, false); + if (this.options?.customJs) { + try { + const $ = load(data); + // CustomJS HERE + data = $.html(); + } catch (error) { + console.error('Error executing customJs:', error); + throw error; + } + } + return ( + data + .match(/<div.*?class="epcontent ([^]*?)<div.*?class="?bottomnav/g)?.[0] + .match(/<p[^>]*>([^]*?)<\/p>/g) + ?.join('\n') || '' + ); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + 'page/' + page + '/?s=' + encodeURIComponent(searchTerm); + const html = await this.safeFecth(url, true); + return this.parseNovels(html); + } +} + +function extractChapterNumber(data: string, tempChapter: Plugin.ChapterItem) { + const tempChapterNumber = data.match(/(\d+)$/); + if (tempChapterNumber && tempChapterNumber[0]) { + tempChapter.chapterNumber = parseInt(tempChapterNumber[0]); + } +} diff --git a/plugins/multisrc/madara/README.md b/plugins/multisrc/madara/README.md new file mode 100644 index 000000000..ed6b23032 --- /dev/null +++ b/plugins/multisrc/madara/README.md @@ -0,0 +1,44 @@ +# Madara multisrc generator + +## Compatiblity + +### This generator is for most sites that uses the Madara WordPress Theme: https://mangabooth.com/ + +it should work for all sites that uses this theme + +to know the name and version of the theme you can enter the url in : [WP Theme Detector](https://www.wpthemedetector.com) +or you can check the version by adding "/wp-content/themes/madara/style.css" +to the site url and check the version in the file + +## Add a new source + +### sources.json + +To add a new source you need to add it to sources.json: + +- id: the id of the source (something unique) +- sourceName: the name of the source (you can use the value of "name" in "https://site.com/wp-json/" if it exists) +- sourceSite: the site url +- options: the options of the source + - lang: the language of the source (default: "English") (check that the language + exists in the languages (check folder names in "plugins/")) + - useNewChapterEndpoint: if the source uses the new chapter endpoint + - versionIncrements: needs to be updated everytime the site url is updated + - customJS: custom javascript that will be excuted when getting the text (if + the site has a custom copyright that need to be removed) + +### icon + +To add an icon to the source you can just run `npm run build:icons` to generate all icons (make sure `npm run build:multisrc` works on your machine) + +Or you can manualy find the icon of the site \ +(try the favicon of the site (https://site.com/favicon.ico) most of the times it redirects you to an image named something like "cropped-site-32x32.png" try to access "cropped-site.png" or "site.png" if that did not work you can try to access "https://site.com/wp-json/" at the end of this very long file there should be a "site_icon_url" value +) (don't forget to convert it to png) +and add it to the folder "public/static/multisrc/madara/{sourceID}/icon.png" + +### filters + +To add filters to a source you need to run the script "get_filters.js" \ +(`npx node plugins/multisrc/madara/get_filters.js` +(if you are at the root of the project) (and you have ran "npm install" before)) +and follow the instructions (url is easier and faster but sometimes it doesn't work) diff --git a/plugins/multisrc/madara/filters/1stkissnovel.json b/plugins/multisrc/madara/filters/1stkissnovel.json new file mode 100644 index 000000000..1dbd4fac5 --- /dev/null +++ b/plugins/multisrc/madara/filters/1stkissnovel.json @@ -0,0 +1,376 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Boy's Love", + "value": "boys-love" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Demons", + "value": "demons" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Food", + "value": "food" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Girls Love", + "value": "girls-love" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Humor", + "value": "humor" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Magical", + "value": "magical" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medical", + "value": "medical" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Moder", + "value": "moder" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Music", + "value": "music" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reverse", + "value": "reverse" + }, + { + "label": "Reverse harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Super power", + "value": "super-power" + }, + { + "label": "Superhero", + "value": "superhero" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Vampire", + "value": "vampire" + }, + { + "label": "Webtoons", + "value": "webtoons" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/Kakikata.json b/plugins/multisrc/madara/filters/Kakikata.json new file mode 100644 index 000000000..7f5fc51b4 --- /dev/null +++ b/plugins/multisrc/madara/filters/Kakikata.json @@ -0,0 +1,116 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "LitRPG", + "value": "litrpg" + }, + { + "label": "Macera", + "value": "macera" + } + ] + }, + "op": { + "type": "Switch", + "label": "VE seçilen tüm türlere sahip olmak", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Sanatçı", + "value": "" + }, + "release": { + "type": "Text", + "label": "Yayınlandığı Yıl", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Yetişkinlere yönelik içerik", + "value": "", + "options": [ + { + "label": "Tümü", + "value": "" + }, + { + "label": "Yetişkin içeriği yok", + "value": "0" + }, + { + "label": "Sadece yetişkinlere yönelik içerik", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durum", + "value": [], + "options": [ + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "Tamamlandı", + "value": "end" + }, + { + "label": "İptal edildi", + "value": "canceled" + }, + { + "label": "Beklemede", + "value": "on-hold" + }, + { + "label": "Yaklaşan", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala", + "value": "", + "options": [ + { + "label": "Alaka düzeyi", + "value": "" + }, + { + "label": "En son", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Değerlendirme", + "value": "rating" + }, + { + "label": "En Çok Görüntüleme", + "value": "views" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/LightNovelUpdates.json b/plugins/multisrc/madara/filters/LightNovelUpdates.json new file mode 100644 index 000000000..138f7991a --- /dev/null +++ b/plugins/multisrc/madara/filters/LightNovelUpdates.json @@ -0,0 +1,344 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Chinese Novel", + "value": "chinese-novel" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Filipino", + "value": "filipino" + }, + { + "label": "Filipino Novel", + "value": "filipino-novel" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Genre", + "value": "genre" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Indonesia", + "value": "indonesia" + }, + { + "label": "Indonesia Novel", + "value": "indonesia-novel" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Khmer", + "value": "khmer" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Korean Novel", + "value": "korean-novel" + }, + { + "label": "Language", + "value": "language" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Malaysian", + "value": "malaysian" + }, + { + "label": "Malaysian Novel", + "value": "malaysian-novel" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "N/A", + "value": "n-a" + }, + { + "label": "Novel Type", + "value": "novel-type" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shotacon", + "value": "shotacon" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life-3" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Thai", + "value": "thai" + }, + { + "label": "Thai Novel", + "value": "thai-novel" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Vietnamese", + "value": "vietnamese" + }, + { + "label": "Vietnamese Novel", + "value": "vietnamese-novel" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/WebNovelTraslation.json b/plugins/multisrc/madara/filters/WebNovelTraslation.json new file mode 100644 index 000000000..543da043e --- /dev/null +++ b/plugins/multisrc/madara/filters/WebNovelTraslation.json @@ -0,0 +1,272 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/ar-mtl.json b/plugins/multisrc/madara/filters/ar-mtl.json new file mode 100644 index 000000000..403426a80 --- /dev/null +++ b/plugins/multisrc/madara/filters/ar-mtl.json @@ -0,0 +1,48 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [] + }, + "op": { + "type": "Switch", + "label": "", + "value": false + }, + "author": { + "type": "Text", + "label": "", + "value": "" + }, + "artist": { + "type": "Text", + "label": "", + "value": "" + }, + "release": { + "type": "Text", + "label": "", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + }, + "status[]": { + "type": "Checkbox", + "label": "", + "value": [], + "options": [] + }, + "m_orderby": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/arnovel.json b/plugins/multisrc/madara/filters/arnovel.json new file mode 100644 index 000000000..919afa80c --- /dev/null +++ b/plugins/multisrc/madara/filters/arnovel.json @@ -0,0 +1,220 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "أكشن", + "value": "أكشن" + }, + { + "label": "إتشي", + "value": "إتشـي" + }, + { + "label": "بالغ", + "value": "بالغ" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "تراجدي", + "value": "تراجدي" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حياة مدرسية", + "value": "حياة-مدرسية" + }, + { + "label": "خارق لطبيعية", + "value": "خارق-لطبيعية" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "راشد", + "value": "راشد" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رومنسي", + "value": "رومنسي" + }, + { + "label": "رياضي", + "value": "رياضي" + }, + { + "label": "سينين", + "value": "سينين" + }, + { + "label": "شريحة من الحياة", + "value": "شريحة-من-الحياة" + }, + { + "label": "شوجو", + "value": "شوجو" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فنون قتال", + "value": "فنون-قتال" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "مغامرات", + "value": "مغامرات" + }, + { + "label": "منتهية", + "value": "منتهية" + }, + { + "label": "ميكا", + "value": "ميكا" + }, + { + "label": "نفسي", + "value": "نفسي" + } + ] + }, + "op": { + "type": "Switch", + "label": "و وجود جميع التصنيفات المختارة", + "value": false + }, + "author": { + "type": "Text", + "label": "المؤلف", + "value": "" + }, + "artist": { + "type": "Text", + "label": "المترجم", + "value": "" + }, + "release": { + "type": "Text", + "label": "سنة الإصدار", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "محتوى للبالغين", + "value": "", + "options": [ + { + "label": "كل", + "value": "" + }, + { + "label": "لا يوجد محتوى للبالغين", + "value": "0" + }, + { + "label": "محتوى للبالغين فقط", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "الحالة", + "value": [], + "options": [ + { + "label": "مستمرة", + "value": "on-going" + }, + { + "label": "منتهية", + "value": "end" + }, + { + "label": "تم إلغاءها", + "value": "canceled" + }, + { + "label": "متوقفة", + "value": "on-hold" + }, + { + "label": "قريباً", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "الترتيب حسب", + "value": "", + "options": [ + { + "label": "ملائمة", + "value": "" + }, + { + "label": "الآخير", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "التقييم", + "value": "rating" + }, + { + "label": "شائع", + "value": "trending" + }, + { + "label": "الأكثر مشاهدة", + "value": "views" + }, + { + "label": "جديد", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/asuralightnovel.json b/plugins/multisrc/madara/filters/asuralightnovel.json new file mode 100644 index 000000000..61fb885d5 --- /dev/null +++ b/plugins/multisrc/madara/filters/asuralightnovel.json @@ -0,0 +1,268 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "academy", + "value": "academy" + }, + { + "label": "Accelerated Growth", + "value": "accelerated-growth" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Ag Jung Seon", + "value": "ag-jung-seon" + }, + { + "label": "bloodline", + "value": "bloodline" + }, + { + "label": "Can Can", + "value": "can-can" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Free", + "value": "free" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/azora.json b/plugins/multisrc/madara/filters/azora.json new file mode 100644 index 000000000..a8b395761 --- /dev/null +++ b/plugins/multisrc/madara/filters/azora.json @@ -0,0 +1,500 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "أكشن", + "value": "أكشن" + }, + { + "label": "أيتام", + "value": "أيتام" + }, + { + "label": "إثارة", + "value": "إثارة" + }, + { + "label": "إعادة إحياء", + "value": "إعادة-إحياء" + }, + { + "label": "اتشي", + "value": "اتشي" + }, + { + "label": "اثارة", + "value": "اثارة" + }, + { + "label": "اسبوعي", + "value": "weekly" + }, + { + "label": "اشباح", + "value": "اشباح" + }, + { + "label": "الأرواح", + "value": "الأرواح" + }, + { + "label": "الحياة المدرسية", + "value": "الحياة-المدرسية" + }, + { + "label": "الحياة اليومية", + "value": "الحياة-اليومية" + }, + { + "label": "السفر عبر الزمن", + "value": "السفر-عبر-الزمن" + }, + { + "label": "العاب", + "value": "العاب" + }, + { + "label": "انتقام", + "value": "انتقام" + }, + { + "label": "ايسكاي", + "value": "ايسكاي" + }, + { + "label": "ايسيكاي", + "value": "ايسيكاي" + }, + { + "label": "ايشي", + "value": "ايشي" + }, + { + "label": "بطل غير اعتيادي", + "value": "بطل-غير-اعتيادي" + }, + { + "label": "بطل مجنون", + "value": "بطل-مجنون" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "تجسد", + "value": "تجسد" + }, + { + "label": "تجسيد", + "value": "تجسيد" + }, + { + "label": "تحقيق", + "value": "تحقيق" + }, + { + "label": "تراجيدي", + "value": "تراجيدي" + }, + { + "label": "تشويق", + "value": "تشويق" + }, + { + "label": "تناسخ", + "value": "تناسخ" + }, + { + "label": "ثأر", + "value": "ثأر" + }, + { + "label": "جريمة", + "value": "جريمة" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "جوسين", + "value": "جوسين" + }, + { + "label": "حاصد", + "value": "حاصد" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حياة جامعية", + "value": "حياة-جامعية" + }, + { + "label": "حياة مدرسية", + "value": "حياة-مدرسية" + }, + { + "label": "خارق للطبيعة", + "value": "خارق-للطبيعة" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "خيالي", + "value": "خيالي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "دموي", + "value": "دموي" + }, + { + "label": "راشد", + "value": "راشد" + }, + { + "label": "رعاية اطفال", + "value": "رعاية-اطفال" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رومانسي", + "value": "رومانسي" + }, + { + "label": "رياضي", + "value": "رياضي" + }, + { + "label": "زمكاني", + "value": "زمكاني" + }, + { + "label": "زمنكاني", + "value": "زمنكاني" + }, + { + "label": "زواج مدبر", + "value": "زواج-مدبر" + }, + { + "label": "زومبي", + "value": "زومبي" + }, + { + "label": "ساموراي", + "value": "ساموراي" + }, + { + "label": "سحر", + "value": "سحر" + }, + { + "label": "سينين", + "value": "سينين" + }, + { + "label": "شريحة من الحياة", + "value": "شريحة-من-الحياة" + }, + { + "label": "شوجو", + "value": "شوجو" + }, + { + "label": "شونسن", + "value": "شونسن" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "شياطين", + "value": "شياطين" + }, + { + "label": "شينين", + "value": "شينين" + }, + { + "label": "طبخ", + "value": "طبخ" + }, + { + "label": "طبي", + "value": "طبي" + }, + { + "label": "عائلي", + "value": "عائلي" + }, + { + "label": "عالم اخر", + "value": "عالم-اخر" + }, + { + "label": "عسكري", + "value": "عسكري" + }, + { + "label": "عصر حديث", + "value": "عصر-حديث" + }, + { + "label": "عصري", + "value": "عصري" + }, + { + "label": "عصور وسطى", + "value": "عصور-وسطى" + }, + { + "label": "عودة بالزمن", + "value": "عودة-بالزمن" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فانتازيا", + "value": "فانتازيا" + }, + { + "label": "فراغ", + "value": "فراغ" + }, + { + "label": "فنتازيا", + "value": "فنتازيا" + }, + { + "label": "فنون قتالية", + "value": "فنون-قتالية" + }, + { + "label": "فيكتوري", + "value": "فيكتوري" + }, + { + "label": "قصة حقيقة", + "value": "قصة-حقيقة" + }, + { + "label": "قوة خارقة", + "value": "قوة-خارقة" + }, + { + "label": "قوى خارقه", + "value": "قوى-خارقه" + }, + { + "label": "كوميدي", + "value": "كوميدي" + }, + { + "label": "لعبة", + "value": "لعبة" + }, + { + "label": "مأساة", + "value": "مأساة" + }, + { + "label": "مأساوي", + "value": "مأساوي" + }, + { + "label": "مافيا", + "value": "مافيا" + }, + { + "label": "مانها", + "value": "مانها" + }, + { + "label": "مانهوا", + "value": "مانهوا" + }, + { + "label": "مصاصي الدماء", + "value": "مصاصي-الدماء" + }, + { + "label": "مغامرات", + "value": "مغامرات" + }, + { + "label": "مغامرة", + "value": "مغامرة" + }, + { + "label": "مكتبي", + "value": "مكتبي" + }, + { + "label": "مميز", + "value": "special" + }, + { + "label": "موريم", + "value": "موريم" + }, + { + "label": "موسيقي", + "value": "موسيقي" + }, + { + "label": "موظفين", + "value": "موظفين" + }, + { + "label": "ندم", + "value": "ندم" + }, + { + "label": "نظام", + "value": "نظام" + }, + { + "label": "نفسي", + "value": "نفسي" + }, + { + "label": "هوس", + "value": "هوس" + }, + { + "label": "وحوش", + "value": "وحوش" + }, + { + "label": "ويبتون", + "value": "ويبتون" + } + ] + }, + "op": { + "type": "Switch", + "label": "مع كل التصنيفات المحددة", + "value": false + }, + "author": { + "type": "Text", + "label": "المؤلف", + "value": "" + }, + "artist": { + "type": "Text", + "label": "الرسام", + "value": "" + }, + "release": { + "type": "Text", + "label": "سنة الاصدار", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "محتوى للبالغين", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "بدون محتوى للبالغين", + "value": "0" + }, + { + "label": "محتوى للبالغين فقط", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "الحالة", + "value": [], + "options": [ + { + "label": "مستمرة", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "ألغيت", + "value": "canceled" + }, + { + "label": "في الانتظار", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "ترتيب حسب :", + "value": "", + "options": [ + { + "label": "ملاءمة", + "value": "" + }, + { + "label": "أحدث", + "value": "latest" + }, + { + "label": "أ-ي", + "value": "alphabet" + }, + { + "label": "تقييم", + "value": "rating" + }, + { + "label": "الشائع", + "value": "trending" + }, + { + "label": "الأكثر مشاهدة", + "value": "views" + }, + { + "label": "جديد", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/azraznovel.json b/plugins/multisrc/madara/filters/azraznovel.json new file mode 100644 index 000000000..07da9a5fe --- /dev/null +++ b/plugins/multisrc/madara/filters/azraznovel.json @@ -0,0 +1,224 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilimkurgu", + "value": "bilimkurgu" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Gender-bender", + "value": "gender-bender" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hayattan Kesitler", + "value": "hayattan-kesitler" + }, + { + "label": "İsekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Korku", + "value": "korku" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Novel", + "value": "novel" + }, + { + "label": "Okul Hayatı", + "value": "okul-hayati" + }, + { + "label": "Oneshot", + "value": "oneshot" + }, + { + "label": "Oyun", + "value": "oyun" + }, + { + "label": "Psikolojik", + "value": "psikolojik" + }, + { + "label": "Renkli", + "value": "renkli" + }, + { + "label": "Romantizm", + "value": "romantizm" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo-ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Tarihi", + "value": "tarihi" + }, + { + "label": "Trajedi", + "value": "trajedi" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yetişkin", + "value": "yetiskin" + } + ] + }, + "op": { + "type": "Switch", + "label": "VE seçilen tüm türlere sahip olmak", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Sanatçı", + "value": "" + }, + "release": { + "type": "Text", + "label": "Çıkış Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Yetişkinlere yönelik içerik", + "value": "", + "options": [ + { + "label": "Tüm", + "value": "" + }, + { + "label": "Yetişkinlere uygun içerik yok", + "value": "0" + }, + { + "label": "Yalnızca yetişkinlere yönelik içerik", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durum", + "value": [], + "options": [ + { + "label": "devam eden", + "value": "on-going" + }, + { + "label": "Tamamlandı", + "value": "end" + }, + { + "label": "İptal edildi", + "value": "canceled" + }, + { + "label": "Beklemede", + "value": "on-hold" + }, + { + "label": "Yaklaşan", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sipariş ver", + "value": "", + "options": [ + { + "label": "Alaka", + "value": "" + }, + { + "label": "En son", + "value": "latest" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/bellereservoir.json b/plugins/multisrc/madara/filters/bellereservoir.json new file mode 100644 index 000000000..271d74d47 --- /dev/null +++ b/plugins/multisrc/madara/filters/bellereservoir.json @@ -0,0 +1,196 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Dark", + "value": "dark" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "R-15", + "value": "r-15" + }, + { + "label": "R-19", + "value": "r-19" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/boxnovel.json b/plugins/multisrc/madara/filters/boxnovel.json new file mode 100644 index 000000000..3d11b419f --- /dev/null +++ b/plugins/multisrc/madara/filters/boxnovel.json @@ -0,0 +1,324 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime & Comics", + "value": "anime-comics" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "Fan-fiction", + "value": "fan-fiction" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Games", + "value": "games" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "General", + "value": "general" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "LitRPG", + "value": "litrpg" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern Life", + "value": "modern-life" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Other", + "value": "other" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "War", + "value": "war" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} diff --git a/plugins/multisrc/madara/filters/citrusaurora.json b/plugins/multisrc/madara/filters/citrusaurora.json new file mode 100644 index 000000000..2b35fd610 --- /dev/null +++ b/plugins/multisrc/madara/filters/citrusaurora.json @@ -0,0 +1,172 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "[ Completed - Locked ]", + "value": "completed-locked" + }, + { + "label": "[ Completed - Unlocked ]", + "value": "completed-unlocked" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Angst", + "value": "angst" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Dark", + "value": "dark" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Romance", + "value": "romances" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Tragedy", + "value": "tragedy" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/coralboutique.json b/plugins/multisrc/madara/filters/coralboutique.json new file mode 100644 index 000000000..e9674cfbd --- /dev/null +++ b/plugins/multisrc/madara/filters/coralboutique.json @@ -0,0 +1,180 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "GL", + "value": "gl" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/daonovel.json b/plugins/multisrc/madara/filters/daonovel.json new file mode 100644 index 000000000..a17c66bd3 --- /dev/null +++ b/plugins/multisrc/madara/filters/daonovel.json @@ -0,0 +1,288 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama-genre" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harems-novel" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "LGBT+", + "value": "lgbt" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts-genre" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo-genre" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Teen", + "value": "teen" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Webcomics", + "value": "webcomics" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/dragonholic.json b/plugins/multisrc/madara/filters/dragonholic.json new file mode 100644 index 000000000..1f34887eb --- /dev/null +++ b/plugins/multisrc/madara/filters/dragonholic.json @@ -0,0 +1,244 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Novel Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "Dropped", + "value": "on-hold" + }, + { + "label": "Hiatus", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/dragontea.json b/plugins/multisrc/madara/filters/dragontea.json new file mode 100644 index 000000000..32995dc9e --- /dev/null +++ b/plugins/multisrc/madara/filters/dragontea.json @@ -0,0 +1,360 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": ["novels"], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "BL/ boy love", + "value": "bl-boy-love" + }, + { + "label": "Children", + "value": "children" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "COMICS", + "value": "comics" + }, + { + "label": "COMPLETE", + "value": "complete" + }, + { + "label": "Contemporary", + "value": "contemporary" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doomsday crisis", + "value": "doomsday-crisis" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Face slapping", + "value": "face-slapping" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "High School", + "value": "high-school" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "INDONESIAN", + "value": "indonesian" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Love", + "value": "love" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Monster", + "value": "monster" + }, + { + "label": "MTL", + "value": "mtl" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "NOVELS", + "value": "novels" + }, + { + "label": "ORIGINAL", + "value": "original" + }, + { + "label": "Powerful", + "value": "powerful" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School", + "value": "school" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shorts", + "value": "shorts" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "shounen ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Time Travel", + "value": "time-travel" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Urban abilities", + "value": "urban-abilities" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + }, + { + "label": "Zombie", + "value": "zombie" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "novels&op&author&artist&release&adult&m_orderby" + }, + { + "label": "Latest", + "value": "" + }, + { + "label": "A-Z", + "value": "" + }, + { + "label": "Rating", + "value": "" + }, + { + "label": "Trending", + "value": "" + }, + { + "label": "Most Views", + "value": "" + }, + { + "label": "New", + "value": "" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/duskblossoms.json b/plugins/multisrc/madara/filters/duskblossoms.json new file mode 100644 index 000000000..d157a157a --- /dev/null +++ b/plugins/multisrc/madara/filters/duskblossoms.json @@ -0,0 +1,252 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Chasing Wife/Runaway", + "value": "chasing-wife-runaway" + }, + { + "label": "Childcare", + "value": "childcare" + }, + { + "label": "Clean & Wholesome", + "value": "clean-wholesome" + }, + { + "label": "Dark Romance", + "value": "dark-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Enemies to Lovers", + "value": "enemies-to-lovers" + }, + { + "label": "Epistolary", + "value": "epistolary" + }, + { + "label": "Fake Relationship", + "value": "fake-relationship" + }, + { + "label": "Forced Proximity", + "value": "forced-proximity-cohabitation" + }, + { + "label": "Friends to Lovers", + "value": "friends-to-lovers" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Modern Romance", + "value": "modern-romance" + }, + { + "label": "Mutual Pining", + "value": "mutual-pining" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Non-Human Characters", + "value": "non-human-characters" + }, + { + "label": "Paranormal Romance", + "value": "paranormal-romance" + }, + { + "label": "Political Intrigue", + "value": "political-intrigue" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "R19", + "value": "r19" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romantic Comedy", + "value": "romantic-comedy" + }, + { + "label": "Romantic Fantasy", + "value": "romantic-fantasy" + }, + { + "label": "Romantic Suspense", + "value": "romantic-suspense" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Second Chance Romance", + "value": "second-chance-romance" + }, + { + "label": "Secret Baby", + "value": "secret-baby" + }, + { + "label": "Short Story", + "value": "short-story" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Workplace Romance", + "value": "workplace-romance" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Most Views", + "value": "views" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/e-novel.json b/plugins/multisrc/madara/filters/e-novel.json new file mode 100644 index 000000000..34fdd8c18 --- /dev/null +++ b/plugins/multisrc/madara/filters/e-novel.json @@ -0,0 +1,192 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Ação", + "value": "acao" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Chinesa", + "value": "chinesa" + }, + { + "label": "Coreana", + "value": "coreana" + }, + { + "label": "Cotidiano", + "value": "cotidiano" + }, + { + "label": "Cultivação", + "value": "cultivacao" + }, + { + "label": "Escolar", + "value": "escolar" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Harém", + "value": "harem" + }, + { + "label": "Indicação", + "value": "indicacao" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Japonesa", + "value": "japonesa" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romance Adulto", + "value": "romance-adulto" + }, + { + "label": "Sistema de Up", + "value": "sistema-de-up" + }, + { + "label": "Sobrenatural", + "value": "sobrenatural" + }, + { + "label": "Super Poderes", + "value": "super-poderes" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Ativo", + "value": "on-going" + }, + { + "label": "Completo", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/ekitaplar.json b/plugins/multisrc/madara/filters/ekitaplar.json new file mode 100644 index 000000000..e5d67fab5 --- /dev/null +++ b/plugins/multisrc/madara/filters/ekitaplar.json @@ -0,0 +1,340 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Aşırı Güçlü", + "value": "asiri-guclu" + }, + { + "label": "Avcı", + "value": "avci" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Canavarlar", + "value": "canavarlar" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Geri Dönen", + "value": "geri-donen" + }, + { + "label": "Gerileme", + "value": "gerileme" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Haremi", + "value": "haremi" + }, + { + "label": "Hayaletler", + "value": "hayaletler" + }, + { + "label": "Hayatın Dilimleri", + "value": "hayatin-dilimleri" + }, + { + "label": "Hile Sistemleri", + "value": "hile-sistemleri" + }, + { + "label": "İntikam", + "value": "intikam" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Kıyamet", + "value": "kiyamet" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Korku", + "value": "korku" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Okul Hayatı", + "value": "okul-hayati" + }, + { + "label": "Oyun", + "value": "oyun" + }, + { + "label": "Psikolojik", + "value": "psikolojik" + }, + { + "label": "Reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Roman", + "value": "roman" + }, + { + "label": "Romantizm", + "value": "romantizm" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Şeytanlar", + "value": "seytanlar" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Sistem", + "value": "sistem" + }, + { + "label": "Spor", + "value": "spor" + }, + { + "label": "Suç", + "value": "suc" + }, + { + "label": "Süper Güç", + "value": "super-guc" + }, + { + "label": "Tamamlanmış", + "value": "tamamlanmis" + }, + { + "label": "Tarihi", + "value": "tarihi" + }, + { + "label": "Tıp", + "value": "tip" + }, + { + "label": "Trajedi", + "value": "trajedi" + }, + { + "label": "Video Oyunları", + "value": "video-oyunlari" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Yeniden Doğuş", + "value": "yeniden-dogus" + }, + { + "label": "Yetişim", + "value": "yetisim" + }, + { + "label": "Yetişkin", + "value": "yetiskin" + }, + { + "label": "Yetişme", + "value": "yetisme" + }, + { + "label": "Zaman Yolculuğu", + "value": "zaman-yolculugu" + }, + { + "label": "Zeki Ana Karakter", + "value": "zeki-ana-karakter" + }, + { + "label": "Zindanlar", + "value": "zindanlar" + }, + { + "label": "Zombi", + "value": "zombi" + } + ] + }, + "op": { + "type": "Switch", + "label": "VE seçilen tüm türlere sahip olmak", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Sanatçı", + "value": "" + }, + "release": { + "type": "Text", + "label": "Çıkış Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Yetişkinlere yönelik içerik", + "value": "", + "options": [ + { + "label": "Tüm", + "value": "" + }, + { + "label": "Yetişkinlere uygun içerik yok", + "value": "0" + }, + { + "label": "Yalnızca yetişkinlere yönelik içerik", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durum", + "value": [], + "options": [ + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "Tamamlandı", + "value": "end" + }, + { + "label": "İptal edildi", + "value": "canceled" + }, + { + "label": "Beklemede", + "value": "on-hold" + }, + { + "label": "Yakında", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala", + "value": "", + "options": [ + { + "label": "Alaka", + "value": "" + }, + { + "label": "En son", + "value": "latest" + }, + { + "label": "A'dan Z'ye", + "value": "alphabet" + }, + { + "label": "Puan", + "value": "rating" + }, + { + "label": "Trendler", + "value": "trending" + }, + { + "label": "Okunma Sayısı", + "value": "views" + }, + { + "label": "Yeni", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/eternalune.json b/plugins/multisrc/madara/filters/eternalune.json new file mode 100644 index 000000000..dd2c4dbcb --- /dev/null +++ b/plugins/multisrc/madara/filters/eternalune.json @@ -0,0 +1,180 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "[Completed]", + "value": "completed" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/etudetranslations.json b/plugins/multisrc/madara/filters/etudetranslations.json new file mode 100644 index 000000000..cbc2f248b --- /dev/null +++ b/plugins/multisrc/madara/filters/etudetranslations.json @@ -0,0 +1,105 @@ +{ + "filters": { + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/fanTLS.json b/plugins/multisrc/madara/filters/fanTLS.json new file mode 100644 index 000000000..2576f9cbe --- /dev/null +++ b/plugins/multisrc/madara/filters/fanTLS.json @@ -0,0 +1,200 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/fanstranslations.json b/plugins/multisrc/madara/filters/fanstranslations.json new file mode 100644 index 000000000..c8407d1ce --- /dev/null +++ b/plugins/multisrc/madara/filters/fanstranslations.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/fortuneeternal.json b/plugins/multisrc/madara/filters/fortuneeternal.json new file mode 100644 index 000000000..32504c6ec --- /dev/null +++ b/plugins/multisrc/madara/filters/fortuneeternal.json @@ -0,0 +1,668 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Abandoned Children", + "value": "abandoned-children" + }, + { + "label": "Academy", + "value": "academy" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adopted Protagonist", + "value": "adopted-protagonist" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Age progression", + "value": "age-progression" + }, + { + "label": "Alternate World", + "value": "alternate-world" + }, + { + "label": "Animated", + "value": "animated" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Apocalypse", + "value": "apocalypse" + }, + { + "label": "Aristocracy", + "value": "aristocracy" + }, + { + "label": "Arts", + "value": "arts" + }, + { + "label": "Award Winning", + "value": "award-winning" + }, + { + "label": "Betrayal", + "value": "betrayal" + }, + { + "label": "Body Swap", + "value": "body-swap" + }, + { + "label": "Business", + "value": "business" + }, + { + "label": "Card Game", + "value": "card-game" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Chaebol", + "value": "chaebol" + }, + { + "label": "Cheat", + "value": "cheat" + }, + { + "label": "Childcare", + "value": "childcare" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Civilization", + "value": "civilization" + }, + { + "label": "Clan Building", + "value": "clan-building-2" + }, + { + "label": "Clan Building]", + "value": "clan-building" + }, + { + "label": "Clever protagonist", + "value": "clever-protagonist" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Dark", + "value": "dark" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Disabilities", + "value": "disabilities" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dying", + "value": "dying" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Evil organization", + "value": "evil-organization" + }, + { + "label": "evil protagonist", + "value": "evil-protagonist" + }, + { + "label": "Exorcism", + "value": "exorcism" + }, + { + "label": "Extra character", + "value": "extra-character" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Farming", + "value": "farming" + }, + { + "label": "Fashion", + "value": "fashion" + }, + { + "label": "Female MC", + "value": "female-mc" + }, + { + "label": "Firearms", + "value": "firearms" + }, + { + "label": "Futuristic", + "value": "futuristic" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Game character", + "value": "game-character" + }, + { + "label": "Game element", + "value": "game-element" + }, + { + "label": "Gate to another world", + "value": "gate-to-another-world" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Genius", + "value": "genius" + }, + { + "label": "Ghost posessed", + "value": "ghost-posessed" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Healthcare", + "value": "healthcare" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "human to animal", + "value": "human-to-animal" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Judicial", + "value": "judicial" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Level system", + "value": "level-system" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Married life", + "value": "married-life" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medical", + "value": "medical" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "misunderstanding", + "value": "misunderstanding" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Monster Life", + "value": "monster-life" + }, + { + "label": "Monster tamer", + "value": "monster-tamer" + }, + { + "label": "MTL", + "value": "mtl" + }, + { + "label": "Music", + "value": "music" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Novel Character", + "value": "novel-character" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Original", + "value": "original" + }, + { + "label": "Outer Space", + "value": "outer-space" + }, + { + "label": "Overpowered", + "value": "overpowered" + }, + { + "label": "Political", + "value": "political" + }, + { + "label": "Polygamy", + "value": "polygamy" + }, + { + "label": "Possesion", + "value": "possesion" + }, + { + "label": "Post-Apocalypse", + "value": "post-apocalypse" + }, + { + "label": "Premium", + "value": "premium" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "RAW", + "value": "raw" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Request", + "value": "request" + }, + { + "label": "Returnee", + "value": "returnee" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romance Fantasy", + "value": "romance-fantasy" + }, + { + "label": "Royal family", + "value": "royal-family" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Science Fiction", + "value": "science-fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Showbiz", + "value": "showbiz" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Strong to stronger", + "value": "strong-to-stronger" + }, + { + "label": "Sudden Rich", + "value": "sudden-rich" + }, + { + "label": "Superhero theme", + "value": "superhero-theme" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Teacher Protagonist", + "value": "teacher-protagonist" + }, + { + "label": "Time", + "value": "time" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Tragic past", + "value": "tragic-past" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Tycoon", + "value": "tycoon" + }, + { + "label": "Villain", + "value": "villain" + }, + { + "label": "Warring period", + "value": "warring-period" + }, + { + "label": "Weak to Strong", + "value": "weak-to-strong" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "World Hopping", + "value": "world-hopping" + }, + { + "label": "Writer", + "value": "writer" + }, + { + "label": "Yandere", + "value": "yandere" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/foxaholic.json b/plugins/multisrc/madara/filters/foxaholic.json new file mode 100644 index 000000000..fe8b6240b --- /dev/null +++ b/plugins/multisrc/madara/filters/foxaholic.json @@ -0,0 +1,276 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Boy X Girl", + "value": "bg" + }, + { + "label": "Boy's Love", + "value": "bl" + }, + { + "label": "Chinese Novel", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Games", + "value": "games" + }, + { + "label": "Girl's Love", + "value": "gl" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Indonesian Novel", + "value": "indonesian" + }, + { + "label": "interstellar", + "value": "interstellar" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Japanese Novel", + "value": "japanese" + }, + { + "label": "Korean Novel", + "value": "korean" + }, + { + "label": "Malay Novel", + "value": "malay" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Original Novel", + "value": "original" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Quick Transmigration", + "value": "quick-transmigration" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Thai Novel", + "value": "thai" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urdu Novel", + "value": "urdu-novel" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Team", + "value": "" + }, + "release": { + "type": "Text", + "label": "Translation Status", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Novel Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/galaxytranslations.json b/plugins/multisrc/madara/filters/galaxytranslations.json new file mode 100644 index 000000000..6773bc146 --- /dev/null +++ b/plugins/multisrc/madara/filters/galaxytranslations.json @@ -0,0 +1,180 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/guavaread.json b/plugins/multisrc/madara/filters/guavaread.json new file mode 100644 index 000000000..afbbbbc25 --- /dev/null +++ b/plugins/multisrc/madara/filters/guavaread.json @@ -0,0 +1,236 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Complete", + "value": "complete" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dropped", + "value": "dropped" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Original Story", + "value": "original-story" + }, + { + "label": "Pilot", + "value": "pilot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "R-15", + "value": "r-15" + }, + { + "label": "R-19", + "value": "r-19" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/hiraethtranslation.json b/plugins/multisrc/madara/filters/hiraethtranslation.json new file mode 100644 index 000000000..c84095916 --- /dev/null +++ b/plugins/multisrc/madara/filters/hiraethtranslation.json @@ -0,0 +1,236 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Novel", + "value": "novel" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Web Novel CN", + "value": "web-novel-cn" + }, + { + "label": "Web Novel JP", + "value": "web-novel" + }, + { + "label": "Web Novel KR", + "value": "web-novel-kr" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/hizomanga.json b/plugins/multisrc/madara/filters/hizomanga.json new file mode 100644 index 000000000..d8472e9cb --- /dev/null +++ b/plugins/multisrc/madara/filters/hizomanga.json @@ -0,0 +1,244 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "آلات", + "value": "mechanisms" + }, + { + "label": "أكشن", + "value": "action" + }, + { + "label": "إثارة", + "value": "excitement" + }, + { + "label": "إيسكاي", + "value": "إيسكاي" + }, + { + "label": "الحياة اليومية", + "value": "slice-of-life" + }, + { + "label": "الحياة مدرسية", + "value": "school-life" + }, + { + "label": "تاريخي", + "value": "historical" + }, + { + "label": "تراجيدي", + "value": "tragic" + }, + { + "label": "جريمة", + "value": "جريمة" + }, + { + "label": "جندر بندر", + "value": "جندر-بندر" + }, + { + "label": "جوسي", + "value": "josei" + }, + { + "label": "حريم", + "value": "harem" + }, + { + "label": "خارق للطبيعة", + "value": "supernatural" + }, + { + "label": "خيال", + "value": "fantasy" + }, + { + "label": "خيال علمي", + "value": "sci-fi" + }, + { + "label": "دراما", + "value": "drama" + }, + { + "label": "دموي", + "value": "دموي" + }, + { + "label": "راشد", + "value": "mature" + }, + { + "label": "رعب", + "value": "horror" + }, + { + "label": "رومانسي", + "value": "romance" + }, + { + "label": "رياضة", + "value": "sports" + }, + { + "label": "زمنكاني", + "value": "my-time" + }, + { + "label": "زومبي", + "value": "زومبي" + }, + { + "label": "سينين", + "value": "seinen" + }, + { + "label": "شريحة من الحياة", + "value": "شريحة-من-الحياة" + }, + { + "label": "شوجو", + "value": "shoujo" + }, + { + "label": "شونين", + "value": "shounen" + }, + { + "label": "طبي", + "value": "طبي" + }, + { + "label": "غموض", + "value": "ambiguity" + }, + { + "label": "فنون قتالية", + "value": "martial-arts" + }, + { + "label": "قوة خارقة", + "value": "superpower" + }, + { + "label": "كوميدي", + "value": "comedy" + }, + { + "label": "مغامرات", + "value": "adventure" + }, + { + "label": "نفسي", + "value": "psychological" + } + ] + }, + "op": { + "type": "Switch", + "label": "مع كل التصنيفات المحددة", + "value": false + }, + "author": { + "type": "Text", + "label": "المؤلف", + "value": "" + }, + "artist": { + "type": "Text", + "label": "المترجم", + "value": "" + }, + "release": { + "type": "Text", + "label": "سنة الإصدار", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "محتوى للبالغين", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "لا يوجد محتوى للبالغين", + "value": "0" + }, + { + "label": "محتوى للبالغين فقط", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "الحالة", + "value": [], + "options": [ + { + "label": "مكتملة", + "value": "complete" + }, + { + "label": "مستمرة", + "value": "on-going" + }, + { + "label": "ألغيت", + "value": "canceled" + }, + { + "label": "قيد الانتظار", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "ترتيب حسب", + "value": "", + "options": [ + { + "label": "ملاءمة", + "value": "" + }, + { + "label": "الأحدث", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "التقييم", + "value": "rating" + }, + { + "label": "الرائج", + "value": "trending" + }, + { + "label": "الأكثر مشاهدة", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/kiniga.json b/plugins/multisrc/madara/filters/kiniga.json new file mode 100644 index 000000000..71b6837ba --- /dev/null +++ b/plugins/multisrc/madara/filters/kiniga.json @@ -0,0 +1,320 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Ação", + "value": "action" + }, + { + "label": "Artes Marciais", + "value": "artes-marciais" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Comédia", + "value": "comedia" + }, + { + "label": "Contemporâneo", + "value": "contemporaneo" + }, + { + "label": "Contos", + "value": "contos-originais" + }, + { + "label": "Crime", + "value": "crime" + }, + { + "label": "Cyberpunk", + "value": "cyberpunk" + }, + { + "label": "Destaque", + "value": "destaque" + }, + { + "label": "Distopia", + "value": "distopia" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Espacial", + "value": "espacial" + }, + { + "label": "Esportes", + "value": "esportes" + }, + { + "label": "Fanfic", + "value": "fanfic" + }, + { + "label": "Fantasia", + "value": "fantasia" + }, + { + "label": "Ficção Científica", + "value": "ficcao-cientifica" + }, + { + "label": "Gastronomia", + "value": "gastronomia" + }, + { + "label": "Harém", + "value": "harem" + }, + { + "label": "Histórico", + "value": "historico" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Infantil", + "value": "infantil" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Jogos", + "value": "jogos" + }, + { + "label": "LGBT+", + "value": "lgbt" + }, + { + "label": "Magia", + "value": "magia" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medieval", + "value": "medieval" + }, + { + "label": "Militar", + "value": "militar" + }, + { + "label": "Mistério", + "value": "misterio" + }, + { + "label": "Mitologia", + "value": "mitologia" + }, + { + "label": "Moderno", + "value": "moderno" + }, + { + "label": "Pós-apocalíptico", + "value": "pos-apocaliptico" + }, + { + "label": "Psicológico", + "value": "psicologico" + }, + { + "label": "Realidade Virtual", + "value": "realidade-virtual" + }, + { + "label": "Recomendado", + "value": "recomendado" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "RPG", + "value": "rpg" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Sistema de Jogo", + "value": "sistema-de-jogo" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "snkhome", + "value": "snkhome" + }, + { + "label": "Sobrenatural", + "value": "sobrenatural" + }, + { + "label": "Somente na Kiniga", + "value": "somente-na-kiniga" + }, + { + "label": "Suspense", + "value": "suspense" + }, + { + "label": "Terror", + "value": "terror" + }, + { + "label": "Terror Psicológico", + "value": "terror-psicologico" + }, + { + "label": "Traduções", + "value": "traducoes" + }, + { + "label": "Tragédia", + "value": "tragedia" + }, + { + "label": "Urbano", + "value": "urbano" + }, + { + "label": "Vida Escolar", + "value": "vida-escolar" + } + ] + }, + "op": { + "type": "Switch", + "label": "Ter todos gêneros selecionados", + "value": false + }, + "author": { + "type": "Text", + "label": "Autor", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artista", + "value": "" + }, + "release": { + "type": "Text", + "label": "Ano", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Conteúdo", + "value": "", + "options": [ + { + "label": "Ambas opções", + "value": "" + }, + { + "label": "Para todas idades", + "value": "0" + }, + { + "label": "Para maiores de idade", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completo", + "value": "complete" + }, + { + "label": "Ativo", + "value": "on-going" + }, + { + "label": "Cancelado", + "value": "canceled" + }, + { + "label": "Pausado", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Ordem por", + "value": "", + "options": [ + { + "label": "Relevância", + "value": "" + }, + { + "label": "Mais antigo", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Avaliação", + "value": "rating" + }, + { + "label": "Tendência", + "value": "trending" + }, + { + "label": "Mais Visualizado", + "value": "views" + }, + { + "label": "Novo", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/lnheaven.json b/plugins/multisrc/madara/filters/lnheaven.json new file mode 100644 index 000000000..fe90d0762 --- /dev/null +++ b/plugins/multisrc/madara/filters/lnheaven.json @@ -0,0 +1,280 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Crossdressing", + "value": "crossdressing" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Female Protagonist", + "value": "female-protagonist" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/lullobox.json b/plugins/multisrc/madara/filters/lullobox.json new file mode 100644 index 000000000..8a8de51ca --- /dev/null +++ b/plugins/multisrc/madara/filters/lullobox.json @@ -0,0 +1,136 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Ancient Must-read", + "value": "ancient-must-read" + }, + { + "label": "Completed Main Story", + "value": "completed-main-story" + }, + { + "label": "KR Ancient", + "value": "r16-kr-ancient-romance-novel" + }, + { + "label": "KR Modern", + "value": "r16-kr-modern-romance-novel" + }, + { + "label": "Modern Must-read", + "value": "modern-must-read" + }, + { + "label": "New 2025", + "value": "new-2025" + }, + { + "label": "Trending", + "value": "trending" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/lunarletters.json b/plugins/multisrc/madara/filters/lunarletters.json new file mode 100644 index 000000000..fcf7d4073 --- /dev/null +++ b/plugins/multisrc/madara/filters/lunarletters.json @@ -0,0 +1,124 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Historical Romance", + "value": "historical-romance" + }, + { + "label": "Modern Romance", + "value": "modern-romance" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/markazriwayat.json b/plugins/multisrc/madara/filters/markazriwayat.json new file mode 100644 index 000000000..1ccae93a7 --- /dev/null +++ b/plugins/multisrc/madara/filters/markazriwayat.json @@ -0,0 +1,224 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "drama", + "value": "drama" + }, + { + "label": "أكشن", + "value": "أكشن" + }, + { + "label": "الحياة الحضرية", + "value": "الحياة-الحضرية" + }, + { + "label": "الزراعة", + "value": "الزراعة" + }, + { + "label": "الهجرة", + "value": "الهجرة" + }, + { + "label": "بناء القواعد", + "value": "بناء-القواعد" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "حرب النجوم", + "value": "حرب-النجوم" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حسم في القتل", + "value": "حسم-في-القتل" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رعب بالغ", + "value": "رعب-بالغ" + }, + { + "label": "سحر", + "value": "سحر" + }, + { + "label": "شريحة من الحياة", + "value": "شريحة-من-الحياة" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "عسكري", + "value": "عسكري" + }, + { + "label": "فانتازيا", + "value": "فانتازيا" + }, + { + "label": "فنون قتالية", + "value": "فنون-قتالية" + }, + { + "label": "قتال", + "value": "قتال" + }, + { + "label": "قوى خارقة", + "value": "قوى-خارقة" + }, + { + "label": "كوارث", + "value": "كوارث" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "لعبة", + "value": "لعبة" + }, + { + "label": "مأساة", + "value": "مأساة" + }, + { + "label": "محاكي", + "value": "محاكي" + }, + { + "label": "مصاصو الدماء", + "value": "مصاصو-الدماء" + }, + { + "label": "مغامرة", + "value": "مغامرة" + }, + { + "label": "مكا", + "value": "مكا" + }, + { + "label": "مهارات القتال", + "value": "مهارات-القتال" + }, + { + "label": "نظام", + "value": "نظام" + }, + { + "label": "نفسي", + "value": "نفسي" + } + ] + }, + "op": { + "type": "Switch", + "label": "و يمتلك جميع التصنيفات المختارة", + "value": false + }, + "author": { + "type": "Text", + "label": "المترجم", + "value": "" + }, + "artist": { + "type": "Text", + "label": "مؤلف", + "value": "" + }, + "release": { + "type": "Text", + "label": "سنة الإصدار", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "المحتوى المخصص للبالغين", + "value": "", + "options": [ + { + "label": "الكل", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "الحالة", + "value": [], + "options": [ + { + "label": "مستمر", + "value": "on-going" + }, + { + "label": "مكتملة", + "value": "end" + }, + { + "label": "ملغاة", + "value": "canceled" + }, + { + "label": "متوقف", + "value": "on-hold" + }, + { + "label": "قادم", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "الأكثر صلة", + "value": "" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/massnovel.json b/plugins/multisrc/madara/filters/massnovel.json new file mode 100644 index 000000000..a829dcc4b --- /dev/null +++ b/plugins/multisrc/madara/filters/massnovel.json @@ -0,0 +1,292 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "action", + "value": "action" + }, + { + "label": "Adulte", + "value": "adulte" + }, + { + "label": "adventure", + "value": "adventure" + }, + { + "label": "Alchimie", + "value": "alchimie" + }, + { + "label": "Arts Martiaux", + "value": "arts-martiaux" + }, + { + "label": "boys", + "value": "boys" + }, + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "Comédie", + "value": "comedie" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "fighting", + "value": "fighting" + }, + { + "label": "fun", + "value": "fun" + }, + { + "label": "Games", + "value": "games" + }, + { + "label": "General", + "value": "general" + }, + { + "label": "girl", + "value": "girl" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "History", + "value": "history" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "horrow", + "value": "horrow" + }, + { + "label": "Idole", + "value": "idole" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Jeux-vidéos", + "value": "jeux-videos" + }, + { + "label": "LGBT+", + "value": "lgbt" + }, + { + "label": "Male Lead", + "value": "male-lead" + }, + { + "label": "manhwa", + "value": "manhwa" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystère", + "value": "mystere" + }, + { + "label": "Paranormal", + "value": "paranormal" + }, + { + "label": "Psychologique", + "value": "psychologique" + }, + { + "label": "Realistic", + "value": "realistic" + }, + { + "label": "Réincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School-Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Surnaturel", + "value": "surnaturel" + }, + { + "label": "Teen", + "value": "teen" + }, + { + "label": "Tragédie", + "value": "tragedie" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "War", + "value": "war" + }, + { + "label": "Wuxia&Xianxia", + "value": "wuxiaxianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/meionovel.json b/plugins/multisrc/madara/filters/meionovel.json new file mode 100644 index 000000000..89c9e19ec --- /dev/null +++ b/plugins/multisrc/madara/filters/meionovel.json @@ -0,0 +1,272 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Virtual Reality", + "value": "virtual-reality" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/meownovel.json b/plugins/multisrc/madara/filters/meownovel.json new file mode 100644 index 000000000..f7cac196b --- /dev/null +++ b/plugins/multisrc/madara/filters/meownovel.json @@ -0,0 +1,236 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "[NSFW]", + "value": "nsfw" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Recommended", + "value": "recommended" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Trend", + "value": "trend" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/morenovel.json b/plugins/multisrc/madara/filters/morenovel.json new file mode 100644 index 000000000..64f25535f --- /dev/null +++ b/plugins/multisrc/madara/filters/morenovel.json @@ -0,0 +1,224 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/mostnovel.json b/plugins/multisrc/madara/filters/mostnovel.json new file mode 100644 index 000000000..b931f393a --- /dev/null +++ b/plugins/multisrc/madara/filters/mostnovel.json @@ -0,0 +1,372 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "ACG", + "value": "acg" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "AGA", + "value": "aga" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Anime & Comics", + "value": "anime-comics" + }, + { + "label": "Bender", + "value": "bender" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "General", + "value": "general" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Horror&Thriller", + "value": "horrorthriller" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Movies", + "value": "movies" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smart MC", + "value": "smart-mc" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/mtl-novel.json b/plugins/multisrc/madara/filters/mtl-novel.json new file mode 100644 index 000000000..403426a80 --- /dev/null +++ b/plugins/multisrc/madara/filters/mtl-novel.json @@ -0,0 +1,48 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [] + }, + "op": { + "type": "Switch", + "label": "", + "value": false + }, + "author": { + "type": "Text", + "label": "", + "value": "" + }, + "artist": { + "type": "Text", + "label": "", + "value": "" + }, + "release": { + "type": "Text", + "label": "", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + }, + "status[]": { + "type": "Checkbox", + "label": "", + "value": [], + "options": [] + }, + "m_orderby": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/mtlnovel.club.json b/plugins/multisrc/madara/filters/mtlnovel.club.json new file mode 100644 index 000000000..3e71fa26e --- /dev/null +++ b/plugins/multisrc/madara/filters/mtlnovel.club.json @@ -0,0 +1,244 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/mysticalmerries.json b/plugins/multisrc/madara/filters/mysticalmerries.json new file mode 100644 index 000000000..3694029ad --- /dev/null +++ b/plugins/multisrc/madara/filters/mysticalmerries.json @@ -0,0 +1,180 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "action", + "value": "action" + }, + { + "label": "adventure", + "value": "adventure" + }, + { + "label": "boys", + "value": "boys" + }, + { + "label": "chinese", + "value": "chinese" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "ecchi", + "value": "ecchi" + }, + { + "label": "fighting", + "value": "fighting" + }, + { + "label": "fun", + "value": "fun" + }, + { + "label": "girl", + "value": "girl" + }, + { + "label": "horrow", + "value": "horrow" + }, + { + "label": "isekai", + "value": "isekai" + }, + { + "label": "manhwa", + "value": "manhwa" + }, + { + "label": "r19", + "value": "r19" + }, + { + "label": "regression", + "value": "regression" + }, + { + "label": "reincarnation", + "value": "reincarnation" + }, + { + "label": "romance fantasy", + "value": "romance-fantasy" + }, + { + "label": "time travel", + "value": "time-travel" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/nabiscans.json b/plugins/multisrc/madara/filters/nabiscans.json new file mode 100644 index 000000000..486dbc4fd --- /dev/null +++ b/plugins/multisrc/madara/filters/nabiscans.json @@ -0,0 +1,188 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "CocaColastic", + "value": "cocacolastic" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Final", + "value": "final" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "novel", + "value": "novel" + }, + { + "label": "Okul Hayatı", + "value": "okul-hayati" + }, + { + "label": "Ptj Serisi", + "value": "ptj-serisi" + }, + { + "label": "Reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Romantizm", + "value": "romantizm" + }, + { + "label": "Spor", + "value": "spor" + }, + { + "label": "Sstem", + "value": "sstem" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Çizer", + "value": "" + }, + "release": { + "type": "Text", + "label": "Yayınlanma Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durumu", + "value": [], + "options": [ + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "Tamamlandı", + "value": "end" + }, + { + "label": "İptal Edildi", + "value": "canceled" + }, + { + "label": "Askıya Alındı", + "value": "on-hold" + }, + { + "label": "Gelecek", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala;", + "value": "", + "options": [ + { + "label": "İlişkin", + "value": "" + }, + { + "label": "En Sondan", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Oylama", + "value": "rating" + }, + { + "label": "Popüler", + "value": "trending" + }, + { + "label": "En Çok Okunan", + "value": "views" + }, + { + "label": "Yeni", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/neosekaiTLS.json b/plugins/multisrc/madara/filters/neosekaiTLS.json new file mode 100644 index 000000000..9518cd8db --- /dev/null +++ b/plugins/multisrc/madara/filters/neosekaiTLS.json @@ -0,0 +1,168 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/nitromanga.json b/plugins/multisrc/madara/filters/nitromanga.json new file mode 100644 index 000000000..f0c48704c --- /dev/null +++ b/plugins/multisrc/madara/filters/nitromanga.json @@ -0,0 +1,336 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Apocalyptic", + "value": "apocalyptic" + }, + { + "label": "Beasts", + "value": "beasts" + }, + { + "label": "Cheat Systems", + "value": "cheat-systems" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Crime", + "value": "crime" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Demons", + "value": "demons" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dungeons", + "value": "dungeons" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Genius MC", + "value": "genius-mc" + }, + { + "label": "Ghosts", + "value": "ghosts" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Hunter", + "value": "hunter" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Medical", + "value": "medical" + }, + { + "label": "Monsters", + "value": "monsters" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Novel", + "value": "novel" + }, + { + "label": "Overpowered", + "value": "overpowered" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Regression", + "value": "regression" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Revenge", + "value": "revenge" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Super Power", + "value": "super-power" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Time Travel", + "value": "time-travel" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Zombie", + "value": "zombie" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/noicetranslations.json b/plugins/multisrc/madara/filters/noicetranslations.json new file mode 100644 index 000000000..542dad39b --- /dev/null +++ b/plugins/multisrc/madara/filters/noicetranslations.json @@ -0,0 +1,268 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "fighting", + "value": "fighting" + }, + { + "label": "fun", + "value": "fun" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "girl", + "value": "girl" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "horrow", + "value": "horrow" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novel-lucky.json b/plugins/multisrc/madara/filters/novel-lucky.json new file mode 100644 index 000000000..500052b47 --- /dev/null +++ b/plugins/multisrc/madara/filters/novel-lucky.json @@ -0,0 +1,384 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + }, + { + "label": "จบแล้ว", + "value": "end" + }, + { + "label": "จันทร์", + "value": "monday" + }, + { + "label": "ทุกวัน", + "value": "everyday" + }, + { + "label": "นิยาย PDF", + "value": "นิยาย-pdf" + }, + { + "label": "นิยายจีน", + "value": "นิยายจีน" + }, + { + "label": "นิยายญี่ปุ่น", + "value": "นิยายญี่ปุ่น" + }, + { + "label": "นิยายดราม่า", + "value": "นิยายดราม่า" + }, + { + "label": "นิยายตลก", + "value": "นิยายตลก" + }, + { + "label": "นิยายทะลุมิติ", + "value": "นิยายทะลุมิติ" + }, + { + "label": "นิยายผจญภัย", + "value": "นิยายผจญภัย" + }, + { + "label": "นิยายผู้ใหญ่", + "value": "นิยายผู้ใหญ่" + }, + { + "label": "นิยายยูริ", + "value": "นิยายยูริ" + }, + { + "label": "นิยายรักโรแมนติก", + "value": "นิยายรักโรแมนติก" + }, + { + "label": "นิยายสยองขวัญ", + "value": "นิยายสยองขวัญ" + }, + { + "label": "นิยายเกาหลี", + "value": "นิยายเกาหลี" + }, + { + "label": "นิยายแฟนตาซี", + "value": "นิยายแฟนตาซี" + }, + { + "label": "นิยายไซไฟ", + "value": "นิยายไซไฟ" + }, + { + "label": "พฤหัสบดี", + "value": "thursday" + }, + { + "label": "พุธ", + "value": "wednesday" + }, + { + "label": "ศุกร์", + "value": "friday" + }, + { + "label": "อังคาร", + "value": "tuesday" + }, + { + "label": "อาทิตย์", + "value": "sunday" + }, + { + "label": "เสาร์", + "value": "saturday" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novel4up.json b/plugins/multisrc/madara/filters/novel4up.json new file mode 100644 index 000000000..18a82ef23 --- /dev/null +++ b/plugins/multisrc/madara/filters/novel4up.json @@ -0,0 +1,212 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "أكشن", + "value": "action" + }, + { + "label": "الخارق للطبيعة", + "value": "supernatural" + }, + { + "label": "تاريخي", + "value": "historical" + }, + { + "label": "تحقيق", + "value": "detective" + }, + { + "label": "تراجيديا", + "value": "tragedy" + }, + { + "label": "حريم", + "value": "harem" + }, + { + "label": "حياة مدرسية", + "value": "school-life" + }, + { + "label": "خيال علمي", + "value": "sci-fi" + }, + { + "label": "دراما", + "value": "drama" + }, + { + "label": "رعب", + "value": "horror" + }, + { + "label": "رومانسي", + "value": "romance" + }, + { + "label": "رياضة", + "value": "sports" + }, + { + "label": "سحر", + "value": "magic" + }, + { + "label": "شريحة من الحياة", + "value": "slice-of-life" + }, + { + "label": "شوانهوا", + "value": "xuanhuan" + }, + { + "label": "شوجو", + "value": "shoujo" + }, + { + "label": "شونين", + "value": "shounen" + }, + { + "label": "غموض", + "value": "mystery" + }, + { + "label": "فانتازيا", + "value": "fantasy" + }, + { + "label": "فنون قتال", + "value": "martial-arts" + }, + { + "label": "كوميديا", + "value": "comedy" + }, + { + "label": "مغامرة", + "value": "adventure" + }, + { + "label": "ميكا", + "value": "mecha" + }, + { + "label": "نفسي", + "value": "psychological" + }, + { + "label": "ون شوت", + "value": "one-shot" + }, + { + "label": "ووشيا", + "value": "wuxia" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "ترتيب حسب:", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "الأحدث", + "value": "latest" + }, + { + "label": "أ-ي", + "value": "alphabet" + }, + { + "label": "التقييم", + "value": "rating" + }, + { + "label": "الشائع", + "value": "trending" + }, + { + "label": "الأكثر مشاهدة", + "value": "views" + }, + { + "label": "الجديد", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelTL.json b/plugins/multisrc/madara/filters/novelTL.json new file mode 100644 index 000000000..13fab272a --- /dev/null +++ b/plugins/multisrc/madara/filters/novelTL.json @@ -0,0 +1,360 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Bleach", + "value": "bleach" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Conan", + "value": "conan" + }, + { + "label": "cooking", + "value": "cooking" + }, + { + "label": "Dragon Ball", + "value": "dragon-ball" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Erciyuan", + "value": "erciyuan" + }, + { + "label": "Fairy Tail", + "value": "fairy-tail" + }, + { + "label": "Faloo", + "value": "faloo" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "FoodWars!", + "value": "foodwars" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Hunter x Hunter", + "value": "hunter-x-hunter" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Jojo", + "value": "jojo" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Marvel", + "value": "marvel" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Naruto", + "value": "naruto" + }, + { + "label": "One piece", + "value": "one-piece" + }, + { + "label": "Pokemon", + "value": "pokemon" + }, + { + "label": "Political", + "value": "political" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sport", + "value": "sport" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Toriko", + "value": "toriko" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelbookid.json b/plugins/multisrc/madara/filters/novelbookid.json new file mode 100644 index 000000000..0da839833 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelbookid.json @@ -0,0 +1,276 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern", + "value": "eastern" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "MartialArts", + "value": "martialarts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelci.json b/plugins/multisrc/madara/filters/novelci.json new file mode 100644 index 000000000..af2e86048 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelci.json @@ -0,0 +1,61 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [] + }, + "op": { + "type": "Switch", + "label": "Having ALL selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [] + }, + "m_orderby": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelmultiverse.json b/plugins/multisrc/madara/filters/novelmultiverse.json new file mode 100644 index 000000000..5a1e74399 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelmultiverse.json @@ -0,0 +1,316 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Original", + "value": "original" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Translation", + "value": "translation" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelninja.json b/plugins/multisrc/madara/filters/novelninja.json new file mode 100644 index 000000000..459807004 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelninja.json @@ -0,0 +1,160 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Childhood Friend", + "value": "childhood-friend" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Economic Thriller", + "value": "economic-thriller" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Kingdom Buidling", + "value": "kingdom-buidling" + }, + { + "label": "Otome Game", + "value": "otome-game" + }, + { + "label": "Production / Producer", + "value": "production-producer" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-Fi", + "value": "sci-fi" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/noveloku.json b/plugins/multisrc/madara/filters/noveloku.json new file mode 100644 index 000000000..31e704990 --- /dev/null +++ b/plugins/multisrc/madara/filters/noveloku.json @@ -0,0 +1,256 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "action" + }, + { + "label": "Bilim-Kurgu", + "value": "sci-fi" + }, + { + "label": "Çağdaş Romantizm", + "value": "contemporary-romance" + }, + { + "label": "Cinsiyet Değiştirme", + "value": "gender-bender" + }, + { + "label": "Doğaüstü", + "value": "supernatural" + }, + { + "label": "Dövüş Sanatları", + "value": "martial-arts" + }, + { + "label": "Dram", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantasy" + }, + { + "label": "Gizem", + "value": "mystery" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Heavenly Silkworm Potato", + "value": "heavenly-silkworm-potato" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "comedy" + }, + { + "label": "Macera", + "value": "adventure" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Okul Hayatı", + "value": "school-life" + }, + { + "label": "Psikolojik", + "value": "psychological" + }, + { + "label": "Romantizm", + "value": "romance" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Tarihsel", + "value": "historical" + }, + { + "label": "Tian Can Tu Dou", + "value": "tian-can-tu-dou" + }, + { + "label": "Trajedi", + "value": "tragedy" + }, + { + "label": "Updating", + "value": "updating" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaşamdan Kesitler", + "value": "slice-of-life" + }, + { + "label": "Yetişkin", + "value": "mature" + }, + { + "label": "Yuri", + "value": "yuri" + }, + { + "label": "天蚕土豆", + "value": "天蚕土豆" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelokutr.json b/plugins/multisrc/madara/filters/novelokutr.json new file mode 100644 index 000000000..a3410c8b9 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelokutr.json @@ -0,0 +1,220 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "action" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Doğa Üstü", + "value": "doga-ustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantasy" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "comedy" + }, + { + "label": "Korku", + "value": "korku" + }, + { + "label": "Macera", + "value": "adventure" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Okul Hayatı", + "value": "school-life" + }, + { + "label": "Oyun", + "value": "oyun" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Tarihi", + "value": "tarihi" + }, + { + "label": "Trajedi", + "value": "trajedi" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaşamdan Kesitler", + "value": "yasamdan-kesitler" + }, + { + "label": "Yetişkin", + "value": "yetiskin" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Çizer", + "value": "" + }, + "release": { + "type": "Text", + "label": "Yayınlanma Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durumu", + "value": [], + "options": [ + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "Tamamlandı", + "value": "end" + }, + { + "label": "İptal Edildi", + "value": "canceled" + }, + { + "label": "Durduruldu", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala;", + "value": "", + "options": [ + { + "label": "İlişkin", + "value": "" + }, + { + "label": "En Sondan", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Oylama", + "value": "rating" + }, + { + "label": "Popüler", + "value": "trending" + }, + { + "label": "En Çok Okunan", + "value": "views" + }, + { + "label": "Yeni", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/novelpdf.json b/plugins/multisrc/madara/filters/novelpdf.json new file mode 100644 index 000000000..53c49f918 --- /dev/null +++ b/plugins/multisrc/madara/filters/novelpdf.json @@ -0,0 +1,268 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "18+", + "value": "18" + }, + { + "label": "กาว", + "value": "กาว" + }, + { + "label": "กำลังภายใน", + "value": "กำลังภายใน" + }, + { + "label": "ของวิเศษ", + "value": "ของวิเศษ" + }, + { + "label": "คลั่งรัก", + "value": "คลั่งรัก" + }, + { + "label": "คอเมดี้", + "value": "คอเมดี้" + }, + { + "label": "ค้าขาย", + "value": "ค้าขาย" + }, + { + "label": "จบ", + "value": "จบ" + }, + { + "label": "จอมมาร", + "value": "จอมมาร" + }, + { + "label": "จีนโบราณ", + "value": "จีนโบราณ" + }, + { + "label": "ชายาอ๋อง", + "value": "ชายาอ๋อง" + }, + { + "label": "ซุปเปอร์สตาร์", + "value": "ซุปเปอร์สตาร์" + }, + { + "label": "ดราม่า", + "value": "ดราม่า" + }, + { + "label": "ต่างโลก", + "value": "ต่างโลก" + }, + { + "label": "ทะลุมิติ", + "value": "ทะลุมิติ" + }, + { + "label": "ทายาทเศรษฐี", + "value": "ทายาทเศรษฐี" + }, + { + "label": "ทำอาหาร", + "value": "ทำอาหาร" + }, + { + "label": "นักปรุงยา", + "value": "นักปรุงยา" + }, + { + "label": "นางเอกเก่ง", + "value": "นางเอกเก่ง" + }, + { + "label": "นิยายจีน", + "value": "นิยายจีน" + }, + { + "label": "นิยายรัก", + "value": "นิยายรัก" + }, + { + "label": "นิยายแปล", + "value": "นิยายแปล" + }, + { + "label": "ผจญภัย", + "value": "ผจญภัย" + }, + { + "label": "ย้อนยุค", + "value": "ย้อนยุค" + }, + { + "label": "ย้อนเวลา", + "value": "ย้อนเวลา" + }, + { + "label": "ระบบ", + "value": "ระบบ" + }, + { + "label": "สงครามอวกาศ", + "value": "สงครามอวกาศ" + }, + { + "label": "สัตวเลี้ยง", + "value": "สัตวเลี้ยง" + }, + { + "label": "สโลไลฟ์", + "value": "สโลไลฟ์" + }, + { + "label": "เกิดใหม่", + "value": "เกิดใหม่" + }, + { + "label": "เทพอสูร", + "value": "เทพอสูร" + }, + { + "label": "เวทมนต์", + "value": "เวทมนต์" + }, + { + "label": "แก้แค้น", + "value": "แก้แค้น" + }, + { + "label": "แฟนตาซี", + "value": "แฟนตาซี" + }, + { + "label": "แอ๊คชัน", + "value": "แอ๊คชัน" + }, + { + "label": "แอคชั่น", + "value": "แอคชั่น" + }, + { + "label": "โรแมนติก", + "value": "โรแมนติก" + }, + { + "label": "ใช้ชีวิต", + "value": "ใช้ชีวิต" + }, + { + "label": "ไซไฟ", + "value": "ไซไฟ" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/olaoe.json b/plugins/multisrc/madara/filters/olaoe.json new file mode 100644 index 000000000..9ba2f8b08 --- /dev/null +++ b/plugins/multisrc/madara/filters/olaoe.json @@ -0,0 +1,732 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "+13", + "value": "13" + }, + { + "label": "+16", + "value": "16" + }, + { + "label": "+17", + "value": "17" + }, + { + "label": "Custom Genre 1", + "value": "custom-genre-1" + }, + { + "label": "Custom Genre 2", + "value": "custom-genre-2" + }, + { + "label": "Custom Genre 3", + "value": "custom-genre-3" + }, + { + "label": "أكشن", + "value": "أكشن" + }, + { + "label": "إثارة", + "value": "إثارة" + }, + { + "label": "إعادة إحياء", + "value": "إعادة-إحياء" + }, + { + "label": "إنتقام", + "value": "إنتقام" + }, + { + "label": "إيتشي", + "value": "إيتشي" + }, + { + "label": "اثارة", + "value": "اثارة" + }, + { + "label": "اثاره", + "value": "اثاره" + }, + { + "label": "اساطير", + "value": "اساطير" + }, + { + "label": "اشباح", + "value": "اشباح" + }, + { + "label": "اضطهاد", + "value": "اضطهاد" + }, + { + "label": "اعادة احياء", + "value": "اعادة-احياء" + }, + { + "label": "اعاده بحث", + "value": "اعاده-بحث" + }, + { + "label": "اقتباس مانجا", + "value": "اقتباس-مانجا" + }, + { + "label": "اقتباس مانهوا", + "value": "اقتباس-مانهوا" + }, + { + "label": "اقتباس مانهوا", + "value": "اقتباس-مانهوا-انمي" + }, + { + "label": "اكشن", + "value": "اكشن" + }, + { + "label": "الحياة المدرسيه", + "value": "الحياة-المدرسيه" + }, + { + "label": "الحياة اليومية", + "value": "الحياة-اليومية" + }, + { + "label": "السفر عبر الزمن", + "value": "السفر-عبر-الزمن" + }, + { + "label": "العاب", + "value": "العاب" + }, + { + "label": "العاب الكترونية", + "value": "العاب-الكترونية" + }, + { + "label": "العاب فيديو", + "value": "العاب-فيديو" + }, + { + "label": "النجاة", + "value": "النجاة" + }, + { + "label": "الهة", + "value": "الهة" + }, + { + "label": "الهه", + "value": "الهه" + }, + { + "label": "الواقع الافتراضي", + "value": "الواقع-الافتراضي" + }, + { + "label": "امرأة شريرة", + "value": "امرأة-شريرة" + }, + { + "label": "انتقام", + "value": "انتقام" + }, + { + "label": "انمي", + "value": "انمي" + }, + { + "label": "انمي ياباني", + "value": "انمي-ياباني" + }, + { + "label": "ايتشى", + "value": "ايتشى" + }, + { + "label": "ايتشي", + "value": "ايتشي" + }, + { + "label": "ايسكاى", + "value": "ايسكاى" + }, + { + "label": "بالغ", + "value": "بالغ" + }, + { + "label": "بطل خارق", + "value": "بطل-خارق" + }, + { + "label": "بطل غير اعتيادي", + "value": "بطل-غير-اعتيادي" + }, + { + "label": "بوليسي", + "value": "بوليسي" + }, + { + "label": "تاريخى", + "value": "تاريخى" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "تجسيد", + "value": "تجسيد" + }, + { + "label": "تحقيق", + "value": "تحقيق" + }, + { + "label": "تراجيدي", + "value": "تراجيدي" + }, + { + "label": "ترجمة جوجل", + "value": "ترجمة-جوجل" + }, + { + "label": "تشويق", + "value": "تشويق" + }, + { + "label": "تناسخ", + "value": "تناسخ" + }, + { + "label": "تناسخ الارواح", + "value": "تناسخ-الارواح" + }, + { + "label": "جريمة", + "value": "جريمة" + }, + { + "label": "جريمه", + "value": "جريمه" + }, + { + "label": "جندر اسواب", + "value": "جندر-اسواب" + }, + { + "label": "جوسى", + "value": "جوسى" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "جوسيه", + "value": "جوسيه" + }, + { + "label": "حائز على جائزة", + "value": "حائز-على-جائزة" + }, + { + "label": "حائز علي جائزة", + "value": "حائز-علي-جائزة" + }, + { + "label": "حديث", + "value": "حديث" + }, + { + "label": "حربى", + "value": "حربى" + }, + { + "label": "حربي", + "value": "حربي" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حياة", + "value": "حياة" + }, + { + "label": "حياة مدرسية", + "value": "حياة-مدرسية" + }, + { + "label": "حياة يومية", + "value": "حياة-يومية" + }, + { + "label": "خارق", + "value": "خارق" + }, + { + "label": "خارق لطبيعية", + "value": "خارق-لطبيعية" + }, + { + "label": "خارق للطبيعة", + "value": "خارق-للطبيعة" + }, + { + "label": "خارق للطبيعه", + "value": "خارق-للطبيعه" + }, + { + "label": "خارق للعادة", + "value": "خارق-للعادة" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمى", + "value": "خيال-علمى" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "خيالي", + "value": "خيالي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "دماء", + "value": "دماء" + }, + { + "label": "دموى", + "value": "دموى" + }, + { + "label": "راشد", + "value": "راشد" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رواية خفيفة", + "value": "رواية-خفيفة" + }, + { + "label": "رومانسى", + "value": "رومانسى" + }, + { + "label": "رومانسي", + "value": "رومانسي" + }, + { + "label": "رياضة", + "value": "رياضة" + }, + { + "label": "رياضه", + "value": "رياضه" + }, + { + "label": "رياضى", + "value": "رياضى" + }, + { + "label": "رياضي", + "value": "رياضي" + }, + { + "label": "زراعة", + "value": "زراعة" + }, + { + "label": "زمكانى", + "value": "زمكانى" + }, + { + "label": "زمكاني", + "value": "زمكاني" + }, + { + "label": "زمنكاني", + "value": "زمنكاني" + }, + { + "label": "زومبي", + "value": "زومبي" + }, + { + "label": "ساخر", + "value": "ساخر" + }, + { + "label": "ساموراي", + "value": "ساموراي" + }, + { + "label": "سباق", + "value": "سباق" + }, + { + "label": "سحر", + "value": "سحر" + }, + { + "label": "سينين", + "value": "سينين" + }, + { + "label": "شرطة", + "value": "شرطة" + }, + { + "label": "شريحة من الحياة", + "value": "شريحة-من-الحياة" + }, + { + "label": "شرير", + "value": "شرير" + }, + { + "label": "شوجو", + "value": "شوجو" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "شياطين", + "value": "شياطين" + }, + { + "label": "صقل", + "value": "صقل" + }, + { + "label": "طبخ", + "value": "طبخ" + }, + { + "label": "ّعامل مكتبي", + "value": "ّعامل-مكتبي" + }, + { + "label": "عسكري", + "value": "عسكري" + }, + { + "label": "عسكريه", + "value": "عسكريه" + }, + { + "label": "علم نفس", + "value": "علم-نفس" + }, + { + "label": "عنف", + "value": "عنف" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فضاء", + "value": "فضاء" + }, + { + "label": "فلسفه", + "value": "فلسفه" + }, + { + "label": "فلم انمي", + "value": "فلم-انمي" + }, + { + "label": "فنتازيا", + "value": "فنتازيا" + }, + { + "label": "فنون قتال", + "value": "فنون-قتال" + }, + { + "label": "فنون قتالية", + "value": "فنون-قتالية" + }, + { + "label": "فنون قتاليه", + "value": "فنون-قتاليه" + }, + { + "label": "قتال", + "value": "قتال" + }, + { + "label": "قوة خارقة", + "value": "قوة-خارقة" + }, + { + "label": "قوى خارقة", + "value": "قوى-خارقة" + }, + { + "label": "كومديا", + "value": "كومديا" + }, + { + "label": "كوميدى", + "value": "كوميدى" + }, + { + "label": "كوميدي", + "value": "كوميدي" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "لعبة", + "value": "لعبة" + }, + { + "label": "لعبه", + "value": "لعبه" + }, + { + "label": "مأساة", + "value": "مأساة" + }, + { + "label": "ماساة", + "value": "ماساة" + }, + { + "label": "مافيا", + "value": "مافيا" + }, + { + "label": "مانجا", + "value": "مانجا" + }, + { + "label": "مانجا على الانترنت", + "value": "مانجا-على-الانترنت" + }, + { + "label": "مانها", + "value": "مانها" + }, + { + "label": "مانهوا", + "value": "مانهوا" + }, + { + "label": "مجموعة قصص", + "value": "مجموعة-قصص" + }, + { + "label": "محاكاة ساخرة", + "value": "محاكاة-ساخرة" + }, + { + "label": "مدرسه", + "value": "مدرسه" + }, + { + "label": "مدرسي", + "value": "مدرسي" + }, + { + "label": "مصاصى الدماء", + "value": "مصاصى-الدماء" + }, + { + "label": "مصاصي دماء", + "value": "مصاصي-دماء" + }, + { + "label": "مغامرات", + "value": "مغامرات" + }, + { + "label": "مغامرة", + "value": "مغامرة" + }, + { + "label": "مقتبسة", + "value": "مقتبسة" + }, + { + "label": "موريم", + "value": "موريم" + }, + { + "label": "موسيقى", + "value": "موسيقى" + }, + { + "label": "موسيقي", + "value": "موسيقي" + }, + { + "label": "ميكا", + "value": "ميكا" + }, + { + "label": "ناضج", + "value": "ناضج" + }, + { + "label": "نظام", + "value": "نظام" + }, + { + "label": "نفسى", + "value": "نفسى" + }, + { + "label": "نفسي", + "value": "نفسي" + }, + { + "label": "نينجا", + "value": "نينجا" + }, + { + "label": "وحوش", + "value": "وحوش" + }, + { + "label": "ويب تون", + "value": "ويب-تون" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "مستمر", + "value": "on-going" + }, + { + "label": "مكتمل", + "value": "end" + }, + { + "label": "ملغى", + "value": "canceled" + }, + { + "label": "في الانتظار", + "value": "on-hold" + }, + { + "label": "قادم قريبا", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/panchotranslations.json b/plugins/multisrc/madara/filters/panchotranslations.json new file mode 100644 index 000000000..eee916e96 --- /dev/null +++ b/plugins/multisrc/madara/filters/panchotranslations.json @@ -0,0 +1,336 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Acción", + "value": "accion" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Aventura", + "value": "aventura" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedia", + "value": "comedia" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Escolar", + "value": "escolar" + }, + { + "label": "Fantasía", + "value": "fantasia" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Magia", + "value": "magia" + }, + { + "label": "Malentendidos", + "value": "malentendidos" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Posesión", + "value": "posesion" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reencarnación", + "value": "reencarnacion" + }, + { + "label": "Regresión", + "value": "regresion" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Silce of Life", + "value": "silce-of-life" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "teniendo todos los géneros seleccionados", + "value": false + }, + "author": { + "type": "Text", + "label": "Autor", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artista", + "value": "" + }, + "release": { + "type": "Text", + "label": "Año de Lanzamiento", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Contenido para Adultos", + "value": "", + "options": [ + { + "label": "Todo", + "value": "" + }, + { + "label": "Ningún contenido para adultos", + "value": "0" + }, + { + "label": "Solo contenido para adultos", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Estado", + "value": [], + "options": [ + { + "label": "Terminada", + "value": "complete" + }, + { + "label": "En curso", + "value": "on-going" + }, + { + "label": "Cancelada", + "value": "canceled" + }, + { + "label": "Abandonada por el Traductor", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Ordenar por", + "value": "", + "options": [ + { + "label": "Importancia", + "value": "" + }, + { + "label": "Más Reciente", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Clasificación", + "value": "rating" + }, + { + "label": "Tendencias", + "value": "trending" + }, + { + "label": "Más vistas", + "value": "views" + }, + { + "label": "Nuevo", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/pasteltales.json b/plugins/multisrc/madara/filters/pasteltales.json new file mode 100644 index 000000000..9c0df480c --- /dev/null +++ b/plugins/multisrc/madara/filters/pasteltales.json @@ -0,0 +1,124 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "G", + "value": "g" + }, + { + "label": "R15", + "value": "r15" + }, + { + "label": "R19", + "value": "r19" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/ragnarscans.json b/plugins/multisrc/madara/filters/ragnarscans.json new file mode 100644 index 000000000..56b905430 --- /dev/null +++ b/plugins/multisrc/madara/filters/ragnarscans.json @@ -0,0 +1,196 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "+18", + "value": "18" + }, + { + "label": "Akademi", + "value": "akademi" + }, + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Doğaüstü", + "value": "dogaustu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Fantezi", + "value": "fantezi" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Kule", + "value": "kule" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Murim", + "value": "murim" + }, + { + "label": "Novel", + "value": "novel" + }, + { + "label": "Reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shuojo", + "value": "shuojo" + }, + { + "label": "Sistem", + "value": "sistem" + }, + { + "label": "Villan", + "value": "villan" + }, + { + "label": "Zindan", + "value": "zindan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Çizer", + "value": "" + }, + "release": { + "type": "Text", + "label": "Yayınlanma Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "Hepsi", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durum", + "value": [], + "options": [ + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "Final", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Yakında", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala;", + "value": "", + "options": [ + { + "label": "İlişkin", + "value": "" + }, + { + "label": "En Sondan", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Oylama", + "value": "rating" + }, + { + "label": "Popüler", + "value": "trending" + }, + { + "label": "En Çok Okunan", + "value": "views" + }, + { + "label": "Yeni", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/ranovel.json b/plugins/multisrc/madara/filters/ranovel.json new file mode 100644 index 000000000..cbce97e48 --- /dev/null +++ b/plugins/multisrc/madara/filters/ranovel.json @@ -0,0 +1,228 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Updating", + "value": "updating" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/readfanfic.json b/plugins/multisrc/madara/filters/readfanfic.json new file mode 100644 index 000000000..41a615bd8 --- /dev/null +++ b/plugins/multisrc/madara/filters/readfanfic.json @@ -0,0 +1,300 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "Cartoon", + "value": "cartoon" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Comic", + "value": "comic" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Original Novel", + "value": "original-novel" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Translated Novel", + "value": "translated-novel" + }, + { + "label": "Webtoon", + "value": "webtoon" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Team", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/riwyat.json b/plugins/multisrc/madara/filters/riwyat.json new file mode 100644 index 000000000..a07f42b27 --- /dev/null +++ b/plugins/multisrc/madara/filters/riwyat.json @@ -0,0 +1,292 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "أكشن", + "value": "أكشن" + }, + { + "label": "أيتشي", + "value": "أيتشي" + }, + { + "label": "استراتجي", + "value": "استراتجي" + }, + { + "label": "العصر الحديث", + "value": "العصر-الحديث" + }, + { + "label": "انتقام", + "value": "انتقام" + }, + { + "label": "بالغ", + "value": "بالغ" + }, + { + "label": "بناء مملكة", + "value": "بناء-مملكة" + }, + { + "label": "بوليسي", + "value": "بوليسي" + }, + { + "label": "تاريخي", + "value": "تاريخي" + }, + { + "label": "جوسي", + "value": "جوسي" + }, + { + "label": "حريم", + "value": "حريم" + }, + { + "label": "حياة يومية", + "value": "حياة-يومية" + }, + { + "label": "خيال", + "value": "خيال" + }, + { + "label": "خيال علمي", + "value": "خيال-علمي" + }, + { + "label": "دراما", + "value": "دراما" + }, + { + "label": "رعب", + "value": "رعب" + }, + { + "label": "رومانسية", + "value": "رومانسية" + }, + { + "label": "رياضي", + "value": "رياضي" + }, + { + "label": "زيانشيا", + "value": "xianxia" + }, + { + "label": "سنين", + "value": "سنين" + }, + { + "label": "شريحة حياة", + "value": "شريحة-حياة" + }, + { + "label": "شوانهوان", + "value": "xuanhuan" + }, + { + "label": "شوجو", + "value": "شوجو" + }, + { + "label": "شونين", + "value": "شونين" + }, + { + "label": "شيانشيا", + "value": "شيانشيا" + }, + { + "label": "عسكري", + "value": "عسكري" + }, + { + "label": "غموض", + "value": "غموض" + }, + { + "label": "فانتازيا", + "value": "فانتازيا" + }, + { + "label": "فنون قتالية", + "value": "فنون-قتالية" + }, + { + "label": "قوى خارقة", + "value": "قوى-خارقة" + }, + { + "label": "كوميديا", + "value": "كوميديا" + }, + { + "label": "كوميك", + "value": "كوميك" + }, + { + "label": "للكبار", + "value": "للكبار" + }, + { + "label": "مأساة", + "value": "مأساة" + }, + { + "label": "مانجا", + "value": "مانجا" + }, + { + "label": "مانها", + "value": "مانها" + }, + { + "label": "مانهوا", + "value": "مانهوا" + }, + { + "label": "مدرسي", + "value": "مدرسي" + }, + { + "label": "مصاصي دماء", + "value": "مصاصي-دماء" + }, + { + "label": "مغامرة", + "value": "مغامرة" + }, + { + "label": "مكتملة", + "value": "مكتملة" + }, + { + "label": "نفسي", + "value": "نفسي" + }, + { + "label": "نهاية العالم", + "value": "نهاية-العالم" + }, + { + "label": "واب تون", + "value": "واب-تون" + }, + { + "label": "وان شوت", + "value": "وان-شوت" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "مستمرة", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/salmonlatte.json b/plugins/multisrc/madara/filters/salmonlatte.json new file mode 100644 index 000000000..460ef186a --- /dev/null +++ b/plugins/multisrc/madara/filters/salmonlatte.json @@ -0,0 +1,224 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Forbidden", + "value": "forbidden" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "R-19", + "value": "r-19" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/sleeptTLS.json b/plugins/multisrc/madara/filters/sleeptTLS.json new file mode 100644 index 000000000..41e4bf9f4 --- /dev/null +++ b/plugins/multisrc/madara/filters/sleeptTLS.json @@ -0,0 +1,268 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Angst", + "value": "angst" + }, + { + "label": "Child Protagonist", + "value": "child-protagonist" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Concubine/s", + "value": "concubine-s" + }, + { + "label": "Demons", + "value": "demons" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Martial Arts", + "value": "martialarts" + }, + { + "label": "Matriarchy", + "value": "matriarchy" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Palace", + "value": "palace" + }, + { + "label": "Politics", + "value": "politics" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Reverse Harem", + "value": "reverse-harem" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "schoollife" + }, + { + "label": "Sci-Fi", + "value": "scifi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "sliceoflife" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Strong Female Lead", + "value": "strong-female-lead" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/sonicmtl.json b/plugins/multisrc/madara/filters/sonicmtl.json new file mode 100644 index 000000000..087d495c6 --- /dev/null +++ b/plugins/multisrc/madara/filters/sonicmtl.json @@ -0,0 +1,300 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Cooking", + "value": "cooking" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Doujinshi", + "value": "doujinshi" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Live action", + "value": "live-action" + }, + { + "label": "Manga", + "value": "manga" + }, + { + "label": "Manhua", + "value": "manhua" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "One shot", + "value": "one-shot" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Soft Yaoi", + "value": "soft-yaoi" + }, + { + "label": "Soft Yuri", + "value": "soft-yuri" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/sweetEscapeTL.json b/plugins/multisrc/madara/filters/sweetEscapeTL.json new file mode 100644 index 000000000..d5f0d74f9 --- /dev/null +++ b/plugins/multisrc/madara/filters/sweetEscapeTL.json @@ -0,0 +1,160 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "R-15", + "value": "r-15" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/sweetescape.json b/plugins/multisrc/madara/filters/sweetescape.json new file mode 100644 index 000000000..d5f0d74f9 --- /dev/null +++ b/plugins/multisrc/madara/filters/sweetescape.json @@ -0,0 +1,160 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Modern", + "value": "modern" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "R-15", + "value": "r-15" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/translatinotaku.json b/plugins/multisrc/madara/filters/translatinotaku.json new file mode 100644 index 000000000..dd773b209 --- /dev/null +++ b/plugins/multisrc/madara/filters/translatinotaku.json @@ -0,0 +1,240 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Antihero Protagonist", + "value": "antihero-protagonist" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Daily Life", + "value": "daily-life" + }, + { + "label": "Dark-Comedy", + "value": "dark-comedy" + }, + { + "label": "Devil fruit", + "value": "devil-fruit" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fanfiction", + "value": "fanfiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Original", + "value": "original" + }, + { + "label": "Revolutionary", + "value": "revolutionary" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Supernat", + "value": "supernat" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Survival", + "value": "survival" + }, + { + "label": "swordsmanship", + "value": "swordsmanship" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "War", + "value": "war" + }, + { + "label": "Weak to Strong", + "value": "weak-to-strong" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/turkcelightnovels.json b/plugins/multisrc/madara/filters/turkcelightnovels.json new file mode 100644 index 000000000..9f67b34dc --- /dev/null +++ b/plugins/multisrc/madara/filters/turkcelightnovels.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Büyü", + "value": "buyu" + }, + { + "label": "Doğa üstü", + "value": "doga-ustu" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Fantazi", + "value": "fantazi" + }, + { + "label": "Gerilim", + "value": "gerilim" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "harem", + "value": "harem" + }, + { + "label": "hareö", + "value": "hareo" + }, + { + "label": "İsekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Okul Hayatı", + "value": "okul-hayati" + }, + { + "label": "Psikolojik", + "value": "psikolojik" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shouju", + "value": "shouju" + }, + { + "label": "Tarihi", + "value": "tarihi" + }, + { + "label": "Web Novel", + "value": "web-novel" + }, + { + "label": "Yaşamdan kesitler", + "value": "yasamdan-kesitler" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/violetlily.json b/plugins/multisrc/madara/filters/violetlily.json new file mode 100644 index 000000000..1d68ceb00 --- /dev/null +++ b/plugins/multisrc/madara/filters/violetlily.json @@ -0,0 +1,196 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "action", + "value": "action" + }, + { + "label": "adult", + "value": "adult" + }, + { + "label": "adventure", + "value": "adventure" + }, + { + "label": "boys", + "value": "boys" + }, + { + "label": "comedy", + "value": "comedy" + }, + { + "label": "completed", + "value": "completed" + }, + { + "label": "drama", + "value": "drama" + }, + { + "label": "fantasy", + "value": "fantasy" + }, + { + "label": "gender bender", + "value": "gender-bender" + }, + { + "label": "harem", + "value": "harem" + }, + { + "label": "josei", + "value": "josei" + }, + { + "label": "manhwa", + "value": "manhwa" + }, + { + "label": "mature", + "value": "mature" + }, + { + "label": "mystery", + "value": "mystery" + }, + { + "label": "psychological", + "value": "psychological" + }, + { + "label": "romance", + "value": "romance" + }, + { + "label": "school life", + "value": "school-life" + }, + { + "label": "slice of life", + "value": "slice-of-life" + }, + { + "label": "smut", + "value": "smut" + }, + { + "label": "supernatural", + "value": "supernatural" + }, + { + "label": "tragedy", + "value": "tragedy" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/wbnovel.json b/plugins/multisrc/madara/filters/wbnovel.json new file mode 100644 index 000000000..709d43f02 --- /dev/null +++ b/plugins/multisrc/madara/filters/wbnovel.json @@ -0,0 +1,256 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Alchemy", + "value": "alchemy" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Crafting", + "value": "crafting" + }, + { + "label": "Cultivation", + "value": "cultivation" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Dungeon Crawling", + "value": "dungeon-crawling" + }, + { + "label": "Eci", + "value": "eci" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Kingdom Building", + "value": "kingdom-building" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Modern Setting", + "value": "modern-setting" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Pets", + "value": "pets" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnator", + "value": "reincarnator" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "Virtual Reality", + "value": "virtual-reality" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/webnoveloku.json b/plugins/multisrc/madara/filters/webnoveloku.json new file mode 100644 index 000000000..04e11ab0e --- /dev/null +++ b/plugins/multisrc/madara/filters/webnoveloku.json @@ -0,0 +1,236 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "+ 18", + "value": "18" + }, + { + "label": "Aksiyon", + "value": "aksiyon" + }, + { + "label": "Bilim Kurgu", + "value": "bilim-kurgu" + }, + { + "label": "Dövüş Sanatları", + "value": "dovus-sanatlari" + }, + { + "label": "Dram", + "value": "dram" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantastik", + "value": "fantastik" + }, + { + "label": "Fantezi", + "value": "fantezi" + }, + { + "label": "Gizem", + "value": "gizem" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Kadın Başrol", + "value": "kadin-basrol" + }, + { + "label": "Kentsel", + "value": "kentsel" + }, + { + "label": "Komedi", + "value": "komedi" + }, + { + "label": "Macera", + "value": "macera" + }, + { + "label": "Manga Listesi", + "value": "manga" + }, + { + "label": "Okul Yaşamı", + "value": "okul-yasami" + }, + { + "label": "Olgun", + "value": "olgun" + }, + { + "label": "Oyun", + "value": "oyun" + }, + { + "label": "Popüler", + "value": "populer" + }, + { + "label": "Reenkarnasyon", + "value": "reenkarnasyon" + }, + { + "label": "Romantik", + "value": "romantik" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Simya", + "value": "simya" + }, + { + "label": "Sistem", + "value": "sistem" + }, + { + "label": "Süpergüç", + "value": "superguc" + }, + { + "label": "Tamamlanmış", + "value": "tamamlanmis" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yeni", + "value": "yeni" + }, + { + "label": "Yetişim", + "value": "yetisim" + }, + { + "label": "Yüksek Kalitede Çevrilmiş", + "value": "yuksek-kalitede-cevrilmis" + } + ] + }, + "op": { + "type": "Switch", + "label": "Having ALL selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Yazar", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Çıkış Yılı", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Durum", + "value": [], + "options": [ + { + "label": "Tamamlandı", + "value": "complete" + }, + { + "label": "Devam Ediyor", + "value": "on-going" + }, + { + "label": "İptal Edildi", + "value": "canceled" + }, + { + "label": "Beklemede", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Sırala", + "value": "", + "options": [ + { + "label": "En Son", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Reyting", + "value": "rating" + }, + { + "label": "Popülarite", + "value": "trending" + }, + { + "label": "Görüntülenme", + "value": "views" + }, + { + "label": "Yeni", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/webnovelover.json b/plugins/multisrc/madara/filters/webnovelover.json new file mode 100644 index 000000000..1e4a36284 --- /dev/null +++ b/plugins/multisrc/madara/filters/webnovelover.json @@ -0,0 +1,240 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adapted into drama", + "value": "adapted-into-drama" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Completed", + "value": "completed" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "English Novel", + "value": "english-novel" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Indo Novel", + "value": "indo-novel" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Spiritual", + "value": "spiritual" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Transmigration", + "value": "transmigration" + }, + { + "label": "University", + "value": "university" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Youth", + "value": "youth" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/wooksteahouse.json b/plugins/multisrc/madara/filters/wooksteahouse.json new file mode 100644 index 000000000..402d1a4c5 --- /dev/null +++ b/plugins/multisrc/madara/filters/wooksteahouse.json @@ -0,0 +1,176 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Historical Fiction", + "value": "historical-fiction" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Humor", + "value": "humor" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Orientation: Hetero/BG", + "value": "orientation-hetero-bg" + }, + { + "label": "Orientation: Yaoi/BL", + "value": "orientation-yaoi-bl" + }, + { + "label": "Orientation: Yuri/GL", + "value": "orientation-yuri-gl" + }, + { + "label": "Origin: China", + "value": "origin-china" + }, + { + "label": "Origin: Japan", + "value": "origin-japan" + }, + { + "label": "Origin: Korea", + "value": "origin-korea" + }, + { + "label": "R18", + "value": "r18" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Science Fiction", + "value": "science-fiction" + }, + { + "label": "Thriller", + "value": "thriller" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/wordexcerpt.json b/plugins/multisrc/madara/filters/wordexcerpt.json new file mode 100644 index 000000000..696523b7a --- /dev/null +++ b/plugins/multisrc/madara/filters/wordexcerpt.json @@ -0,0 +1,82 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [] + }, + "op": { + "type": "Switch", + "label": "Having ALL selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "", + "value": "", + "options": [] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/worldnovel.json b/plugins/multisrc/madara/filters/worldnovel.json new file mode 100644 index 000000000..277c53f33 --- /dev/null +++ b/plugins/multisrc/madara/filters/worldnovel.json @@ -0,0 +1,204 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Arts martiaux", + "value": "arts-martiaux" + }, + { + "label": "Aventure", + "value": "aventure" + }, + { + "label": "Comédie", + "value": "comedie" + }, + { + "label": "Drame", + "value": "drame" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantaisie", + "value": "fantaisie" + }, + { + "label": "Horreur", + "value": "horreur" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Light Novel", + "value": "light-novel" + }, + { + "label": "Mystère", + "value": "mystere" + }, + { + "label": "Novel Originaux", + "value": "novel-originaux" + }, + { + "label": "Novel Publié", + "value": "novel-publie" + }, + { + "label": "Psychologique", + "value": "psychologique" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Sport", + "value": "sport" + }, + { + "label": "Surnaturel", + "value": "surnaturel" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragédie", + "value": "tragedie" + }, + { + "label": "Tranche de vie", + "value": "tranche-de-vie" + }, + { + "label": "Web Novel", + "value": "web-novel" + } + ] + }, + "op": { + "type": "Switch", + "label": "ET avoir tous les genres sélectionnés", + "value": false + }, + "author": { + "type": "Text", + "label": "Auteur", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artiste", + "value": "" + }, + "release": { + "type": "Text", + "label": "Année de sortie", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Contenu pour adultes", + "value": "", + "options": [ + { + "label": "Tous", + "value": "" + }, + { + "label": "Aucun contenu adulte", + "value": "0" + }, + { + "label": "Contenu pour adultes uniquement", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Statut", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Trier par", + "value": "", + "options": [ + { + "label": "Pertinence", + "value": "" + }, + { + "label": "Les plus récents", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Classement", + "value": "rating" + }, + { + "label": "Tendance", + "value": "trending" + }, + { + "label": "La plupart des vues", + "value": "views" + }, + { + "label": "Nouveau", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/wuxiaworld.site.json b/plugins/multisrc/madara/filters/wuxiaworld.site.json new file mode 100644 index 000000000..a17c66bd3 --- /dev/null +++ b/plugins/multisrc/madara/filters/wuxiaworld.site.json @@ -0,0 +1,288 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Drama", + "value": "drama-genre" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harems-novel" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "LGBT+", + "value": "lgbt" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Manhwa", + "value": "manhwa" + }, + { + "label": "Martial Arts", + "value": "martial-arts-genre" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo-genre" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Teen", + "value": "teen" + }, + { + "label": "Thriller", + "value": "thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Webcomics", + "value": "webcomics" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "OnGoing", + "value": "on-going" + }, + { + "label": "Completed", + "value": "end" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + }, + { + "label": "Upcoming", + "value": "upcoming" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/filters/zetroTL.json b/plugins/multisrc/madara/filters/zetroTL.json new file mode 100644 index 000000000..7050e479c --- /dev/null +++ b/plugins/multisrc/madara/filters/zetroTL.json @@ -0,0 +1,192 @@ +{ + "filters": { + "genre[]": { + "type": "Checkbox", + "label": "Genre", + "value": [], + "options": [ + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Dark Elf", + "value": "dark-elf" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "NTR", + "value": "ntr" + }, + { + "label": "Original Works", + "value": "original-works" + }, + { + "label": "Rom-Com", + "value": "rom-com" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School", + "value": "school" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Villain", + "value": "villain" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "op": { + "type": "Switch", + "label": "having all selected genres", + "value": false + }, + "author": { + "type": "Text", + "label": "Author", + "value": "" + }, + "artist": { + "type": "Text", + "label": "Artist", + "value": "" + }, + "release": { + "type": "Text", + "label": "Year of Released", + "value": "" + }, + "adult": { + "type": "Picker", + "label": "Adult content", + "value": "", + "options": [ + { + "label": "All", + "value": "" + }, + { + "label": "None adult content", + "value": "0" + }, + { + "label": "Only adult content", + "value": "1" + } + ] + }, + "status[]": { + "type": "Checkbox", + "label": "Status", + "value": [], + "options": [ + { + "label": "Completed", + "value": "complete" + }, + { + "label": "Ongoing", + "value": "on-going" + }, + { + "label": "Canceled", + "value": "canceled" + }, + { + "label": "On Hold", + "value": "on-hold" + } + ] + }, + "m_orderby": { + "type": "Picker", + "label": "Order by", + "value": "", + "options": [ + { + "label": "Relevance", + "value": "" + }, + { + "label": "Latest", + "value": "latest" + }, + { + "label": "A-Z", + "value": "alphabet" + }, + { + "label": "Rating", + "value": "rating" + }, + { + "label": "Trending", + "value": "trending" + }, + { + "label": "Most Views", + "value": "views" + }, + { + "label": "New", + "value": "new-manga" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/madara/generator.js b/plugins/multisrc/madara/generator.js new file mode 100644 index 000000000..cb3fa09c7 --- /dev/null +++ b/plugins/multisrc/madara/generator.js @@ -0,0 +1,39 @@ +import list from './sources.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(source => { + const exist = existsSync(join(folder, 'filters', source.id + '.json')); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.id + '.json'), + ); + source.filters = JSON.parse(filters).filters; + } + console.log( + `[madara] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const madaraTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${madaraTemplate.replace('// CustomJS HERE', source.options?.customJs || '')} +const plugin = new MadaraPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + return { + lang: source.options?.lang || 'English', + filename: source.sourceName.replace(/[^\w]/g, ''), + pluginScript, + }; +}; diff --git a/plugins/multisrc/madara/get_filters.js b/plugins/multisrc/madara/get_filters.js new file mode 100644 index 000000000..a766b4fea --- /dev/null +++ b/plugins/multisrc/madara/get_filters.js @@ -0,0 +1,248 @@ +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import * as readline from 'readline'; +import process from 'process'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function getFilters(name, html) { + const $ = cheerio.load(html); + const filters = { + filters: { + 'genre[]': { + type: 'Checkbox', + label: 'Genre', + value: [], + options: [], + }, + 'op': { + type: 'Switch', + label: 'Having ALL selected genres', + value: false, + }, + 'author': { + type: 'Text', + label: 'Author', + value: '', + }, + 'artist': { + type: 'Text', + label: 'Artist', + value: '', + }, + 'release': { + type: 'Text', + label: 'Year of Released', + value: '', + }, + 'adult': { + type: 'Picker', + label: 'Adult content', + value: '', + options: [], + }, + 'status[]': { + type: 'Checkbox', + label: 'Status', + value: [], + options: [], + }, + 'm_orderby': { + type: 'Picker', + label: 'Order by', + value: '', + options: [], + }, + }, + }; + + const form = $('form.search-advanced-form'); + + // ==================== Genre ==================== + form.find('input[name="genre[]"]').each((i, el) => { + const option = { + label: $(el).next('label').text().trim(), + value: decodeURI($(el).attr('value') || ''), + }; + filters.filters['genre[]'].options.push(option); + }); + + // ===================== Op ====================== + filters.filters['op'].label = + form + .find('select[name="op"] > option') + .eq(1) + .text() + .replace('AND', '') + .replace('(', '') + .replace(')', '') + .trim() || 'Having ALL selected genres'; + + // ==================== Author ==================== + filters.filters['author'].label = + form.find('input[name="author"]').prev('span').text().trim() || 'Author'; + + // ==================== Artist ==================== + filters.filters['artist'].label = + form.find('input[name="artist"]').prev('span').text().trim() || 'Artist'; + + // ==================== Release ==================== + filters.filters['release'].label = + form.find('input[name="release"]').prev('span').text().trim() || + 'Year of Released'; + + // ==================== Adult ==================== + filters.filters['adult'].label = + form.find('select[name="adult"]').prev('span').text().trim() || + 'Adult content'; + form.find('select[name="adult"] > option').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: decodeURI($(el).attr('value') || ''), + }; + filters.filters['adult'].options.push(option); + }); + if (filters.filters['adult'].options.length == 0) { + filters.filters['adult'].options = [ + { label: 'All', value: '' }, + { label: 'None adult content', value: '0' }, + { label: 'Only adult content', value: '1' }, + ]; + } + + // ==================== Status ==================== + filters.filters['status[]'].label = + form.find('input[name="status[]"]').parent().prev('span').text().trim() || + 'Status'; + form + .find('input[name="status[]"]') + .next('label') + .each((i, el) => { + const option = { + label: $(el).text().trim(), + value: $(el).attr('for'), + }; + filters.filters['status[]'].options.push(option); + }); + + // ==================== Order by ==================== + const orderByDiv = $('div.c-nav-tabs').eq(0); + filters.filters['m_orderby'].label = orderByDiv.find('span').text().trim(); + orderByDiv.find('a').each((i, el) => { + const option = { + label: $(el).text().trim(), + value: decodeURI( + ($(el).attr('href')?.split('=').length == 3 + ? $(el).attr('href')?.split('=')[2] + : '') || '', + ), + }; + filters.filters['m_orderby'].options.push(option); + }); + + if ( + filters.filters['m_orderby'].options.length == 0 || + filters.filters['status[]'].options.length == 0 || + filters.filters['adult'].options.length == 0 || + filters.filters['genre[]'].options.length == 0 + ) { + console.error( + `🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + '.json')})🚨`, + ); + } + + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + JSON.stringify(filters, null, 2), + ); + console.log(`✅Filters created successfully for ${name}✅`); +} + +async function getFiltersFromURL(name, url) { + const response = await fetch(url + '/?s=&post_type=wp-manga'); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}, while fetching ${response.url}`, + ); + } + const html = await response.text(); + try { + getFilters(name, html); + } catch (e) { + console.error('Error while getting filters from', url); + } +} + +async function askGetFilter() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await rl.question( + 'Enter the id of the site (same one as in sources.json): ', + async name => { + await rl.question( + EREASE_PREV_LINE + + "Do you want to get the filters from a URL or the html text? (if url dosen't work try html) (url/html): ", + async method => { + if (method.toLowerCase() === 'url') { + const sources = JSON.parse( + fs.readFileSync(path.join(__dirname, 'sources.json'), 'utf-8'), + ); + const source = sources.find(s => s.id === name); + if (source && source.sourceSite) { + console.log('Getting filters from', source.sourceSite); + try { + await getFiltersFromURL(name, source.sourceSite); + } catch (e) { + console.error( + 'Error while getting filters from', + source.sourceSite, + ); + console.log(e.message || e); + } + rl.close(); + } else { + await rl.question( + EREASE_PREV_LINE + + 'Enter the URL (same one as in sources.json): ', + async url => { + rl.close(); + try { + await getFiltersFromURL(name, url); + } catch (e) { + console.error('Error while getting filters from', url); + console.log(e.message || e); + } + }, + ); + } + } else { + process.stdout.write( + EREASE_PREV_LINE + + `Enter the html text from the page at {sourceSite}/?s=&post_type=wp-manga (at the end press ENTER then press CTRL+C): `, + ); + let html = ''; + rl.on('SIGINT', () => { + console.log('Stopeed reading input, creating filters file'); + getFilters(name, html); + rl.close(); + }); + rl.on('line', line => { + html += line + '\n'; + }); + } + }, + ); + }, + ); +} + +askGetFilter(); + +export { getFiltersFromURL }; diff --git a/plugins/multisrc/madara/sources.json b/plugins/multisrc/madara/sources.json new file mode 100644 index 000000000..7f2f12720 --- /dev/null +++ b/plugins/multisrc/madara/sources.json @@ -0,0 +1,664 @@ +[ + { + "id": "novelTL", + "sourceSite": "https://noveltranslate.com/", + "sourceName": "NovelTranslate", + "options": { + "down": true, + "downSince": 1768289212925 + } + }, + { + "id": "lunarletters", + "sourceSite": "https://lunarletters.com/", + "sourceName": "LunarLetters", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "sleeptTLS", + "sourceSite": "https://sleepytranslations.com/", + "sourceName": "SleepyTranslations", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "1stkissnovel", + "sourceSite": "https://1stkissnovel.org/", + "sourceName": "FirstKissNovel", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "daonovel", + "sourceSite": "https://daonovel.co/", + "sourceName": "DaoNovel", + "options": { + "useNewChapterEndpoint": true, + "down": true, + "downSince": 1777153113000 + } + }, + { + "id": "mostnovel", + "sourceSite": "https://mostnovel.com/", + "sourceName": "MostNovel", + "options": {} + }, + { + "id": "novelmultiverse", + "sourceSite": "https://www.novelmultiverse.com/", + "sourceName": "NovelMultiverse" + }, + { + "id": "lnheaven", + "sourceSite": "https://lightnovelheaven.com/", + "sourceName": "LightNovelHeaven", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "arnovel", + "sourceSite": "https://ar-no.com/", + "sourceName": "ArNovel", + "options": { + "useNewChapterEndpoint": true, + "lang": "Arabic" + } + }, + { + "id": "meionovel", + "sourceSite": "https://meionovels.com/", + "sourceName": "MeioNovel", + "options": { + "useNewChapterEndpoint": true, + "lang": "Indonesian" + } + }, + { + "id": "webnovelover", + "sourceSite": "https://www.webnovelover.com/", + "sourceName": "WebNovelLover", + "options": { + "down": true, + "downSince": 1768289212967 + } + }, + { + "id": "wbnovel", + "sourceSite": "https://wbnovel.com/", + "sourceName": "WBNovel", + "options": { + "lang": "Indonesian" + } + }, + { + "id": "wuxiaworld.site", + "sourceSite": "https://wuxiaworld.site/", + "sourceName": "WuxiaWorld.Site", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "mysticalmerries", + "sourceSite": "https://mysticalmerries.com/", + "sourceName": "MysticalSeries", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "morenovel", + "sourceSite": "https://vanovel.com/", + "sourceName": "Vanovel", + "options": { + "useNewChapterEndpoint": true, + "lang": "Indonesian" + } + }, + { + "id": "hizomanga", + "sourceSite": "https://hizomanga.net/", + "sourceName": "HizoManga", + "options": { + "useNewChapterEndpoint": true, + "lang": "Arabic" + } + }, + { + "id": "riwyat", + "sourceSite": "https://cenele.com/", + "sourceName": "Riwyat", + "options": { + "useNewChapterEndpoint": true, + "lang": "Arabic", + "customJs": "chapterText.find('span[style*=\"opacity: 0; position: fixed;\"],[role=\"presentation\"]').remove();" + } + }, + { + "id": "novel4up", + "sourceSite": "https://novel4up.com/", + "sourceName": "Novel4Up", + "options": { + "lang": "Arabic", + "down": true, + "downSince": 1777153113000 + } + }, + { + "id": "turkcelightnovels", + "sourceSite": "https://turkcelightnovels.com/", + "sourceName": "TurkceLightNovels", + "options": { + "lang": "Turkish" + } + }, + { + "id": "sonicmtl", + "sourceSite": "https://www.sonicmtl.com/", + "sourceName": "SonicMTL", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "guavaread", + "sourceSite": "https://guavaread.com/", + "sourceName": "Guavaread", + "options": { + "useNewChapterEndpoint": true, + "down": true, + "downSince": 1768289212912 + } + }, + { + "id": "neosekaiTLS", + "sourceSite": "https://www.neosekaitranslations.com/", + "sourceName": "NeoSekai Translations" + }, + { + "id": "mtl-novel", + "sourceSite": "https://mtl-novel.com/", + "sourceName": "MTL-Novel", + "options": { + "useNewChapterEndpoint": false, + "down": true, + "downSince": 1768289212922 + } + }, + { + "id": "zetroTL", + "sourceSite": "https://zetrotranslation.com/", + "sourceName": "Zetro Translation", + "options": { + "hasLocked": true + } + }, + { + "id": "webnoveloku", + "sourceSite": "https://www.webnoveloku.com/", + "sourceName": "WebNovelOku", + "options": { + "lang": "Turkish" + } + }, + { + "id": "LightNovelUpdates", + "sourceSite": "https://www.lightnovelupdates.com/", + "sourceName": "Light Novel Updates", + "options": { + "down": true, + "downSince": 1768289212916 + } + }, + { + "id": "foxaholic", + "sourceSite": "https://www.foxaholic.com/", + "sourceName": "Foxaholic", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "nitromanga", + "sourceSite": "https://nitromanga.com/", + "sourceName": "Nitro Manga", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "kiniga", + "sourceSite": "https://kiniga.com/", + "sourceName": "Kiniga", + "options": { + "lang": "Portuguese", + "down": true, + "downSince": 1768289212949 + } + }, + { + "id": "panchotranslations", + "sourceSite": "https://panchotranslations.com/", + "sourceName": "Pancho Translations", + "options": { + "useNewChapterEndpoint": true, + "lang": "Spanish" + } + }, + { + "id": "salmonlatte", + "sourceSite": "https://salmonlatte.com/", + "sourceName": "Salmon Latte", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "asuralightnovel", + "sourceSite": "https://srankmanga.com/", + "sourceName": "Srank Manga", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "novel-lucky", + "sourceSite": "https://novel-lucky.com/", + "sourceName": "Novel Lucky", + "options": { + "useNewChapterEndpoint": true, + "lang": "Thai" + } + }, + { + "id": "fanstranslations", + "sourceSite": "https://fanstranslations.com/", + "sourceName": "Fans Translations", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "worldnovel", + "sourceSite": "https://world-novel.fr/", + "sourceName": "WorldNovel", + "options": { + "useNewChapterEndpoint": true, + "lang": "French" + } + }, + { + "id": "hiraethtranslation", + "sourceSite": "https://hiraethtranslation.com/", + "sourceName": "Hiraeth Translation", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "novelpdf", + "sourceSite": "https://novelpdf.xyz/", + "sourceName": "Novel PDF", + "options": { + "useNewChapterEndpoint": true, + "lang": "Thai" + } + }, + { + "id": "azora", + "sourceSite": "https://azoramoon.com/", + "sourceName": "Azora", + "options": { + "useNewChapterEndpoint": true, + "lang": "Arabic" + } + }, + { + "id": "translatinotaku", + "sourceSite": "https://translatinotaku.net/", + "sourceName": "TranslatinOtaku", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "dragontea", + "sourceSite": "https://dragontea.ink/", + "sourceName": "Dragon Tea", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "novelbookid", + "sourceSite": "https://www.novelbook.id/", + "sourceName": "NovelBookID", + "options": { + "useNewChapterEndpoint": true, + "lang": "Indonesian" + } + }, + { + "id": "meownovel", + "sourceSite": "https://meownovel.com/", + "sourceName": "Meownovel", + "options": { + "useNewChapterEndpoint": true, + "lang": "English", + "down": true, + "downSince": 1768289212931 + } + }, + { + "id": "galaxytranslations", + "sourceSite": "https://darkstartranslations.com/", + "sourceName": "Galaxy Translations", + "options": { + "useNewChapterEndpoint": true, + "lang": "English", + "down": true, + "downSince": 1768289212909 + } + }, + { + "id": "olaoe", + "sourceSite": "https://olaoe.cyou/", + "sourceName": "Olaoe.cyou", + "options": { + "useNewChapterEndpoint": true, + "lang": "Arabic", + "down": true, + "downSince": 1768289212938 + } + }, + { + "id": "wordexcerpt", + "sourceSite": "https://wordexcerpt.com/", + "sourceName": "WordExcerpt", + "options": { + "lang": "English" + } + }, + { + "id": "citrusaurora", + "sourceSite": "https://citrusaurora.com/", + "sourceName": "Citrus Aurora", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "duskblossoms", + "sourceSite": "https://duskblossoms.com/", + "sourceName": "Dusk Blossoms", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "bellereservoir", + "sourceSite": "https://bellereservoir.com/", + "sourceName": "Belle Reservoir", + "options": { + "useNewChapterEndpoint": true, + "lang": "English", + "down": true, + "downSince": 1777153113000 + } + }, + { + "id": "traducciones", + "sourceSite": "https://traduccionesamistosas.topmanhuas.org/", + "sourceName": "Traducciones Amistosas", + "options": { + "useNewChapterEndpoint": true, + "lang": "Spanish" + } + }, + { + "id": "coralboutique", + "sourceSite": "https://coralboutique.xyz/", + "sourceName": "Coral Boutique", + "options": { + "useNewChapterEndpoint": true, + "lang": "English", + "down": true, + "downSince": 1777153113000 + } + }, + { + "id": "pasteltales", + "sourceSite": "https://pasteltales.com/", + "sourceName": "Pastel Tales", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "azraznovel", + "sourceSite": "https://araznovel.com/", + "sourceName": "Araz Novel", + "options": { + "useNewChapterEndpoint": true, + "lang": "Turkish" + } + }, + { + "id": "noveloku", + "sourceSite": "https://noveloku.com.tr/", + "sourceName": "Novel oku", + "options": { + "useNewChapterEndpoint": true, + "lang": "Turkish", + "down": true, + "downSince": 1768289212959 + } + }, + { + "id": "nabiscans", + "sourceSite": "https://nabiscans.com/", + "sourceName": "NABİ SCANS", + "options": { + "useNewChapterEndpoint": true, + "lang": "Turkish", + "down": true, + "downSince": 1768289212965 + } + }, + { + "id": "ekitaplar", + "sourceSite": "https://e-kitaplar.com/", + "sourceName": "E-KİTAPLAR", + "options": { + "useNewChapterEndpoint": true, + "lang": "Turkish" + } + }, + { + "id": "wooksteahouse", + "sourceSite": "https://wooksteahouse.com/", + "sourceName": "Wook's Teahouse", + "options": { + "useNewChapterEndpoint": true, + "lang": "English" + } + }, + { + "id": "eternalune", + "sourceName": "Eternalune", + "sourceSite": "https://eternalune.com/", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "fortuneeternal", + "sourceSite": "https://www.fortuneeternal.com", + "sourceName": "Fortune Eternal", + "options": { + "lang": "Korean", + "useNewChapterEndpoint": true + } + }, + { + "id": "ragnarscans", + "sourceSite": "https://ragnarscans.com/", + "sourceName": "Ragnar Scans", + "options": { + "lang": "Turkish", + "useNewChapterEndpoint": true + } + }, + { + "id": "readfanfic", + "sourceSite": "https://readfanfic.com/", + "sourceName": "Read Fanfic", + "options": { + "lang": "English" + } + }, + { + "id": "violetlily", + "sourceSite": "https://violetlilytranslation.com/", + "sourceName": "Violet Lily", + "options": { + "lang": "English", + "useNewChapterEndpoint": true, + "down": true, + "downSince": 1768289212936 + } + }, + { + "id": "sweetescape", + "sourceSite": "https://sweetescapetranslations.com/", + "sourceName": "Sweet Escape", + "options": { + "lang": "English", + "useNewChapterEndpoint": true, + "down": true, + "downSince": 1768289212933 + } + }, + { + "id": "WebNovelTraslation", + "sourceSite": "https://webnoveltranslations.com/", + "sourceName": "Web Novel Translation", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "AnimesHoy12", + "sourceSite": "https://animeshoy12.com/", + "sourceName": "AnimesHoy12", + "options": { + "lang": "Spanish", + "down": true, + "downSince": 1768289212951 + } + }, + { + "id": "Kakikata", + "sourceSite": "https://kakikata.com.tr/", + "sourceName": "kakikata", + "options": { + "lang": "Turkish", + "useNewChapterEndpoint": true + } + }, + { + "id": "ranovel", + "sourceSite": "https://ranovel.com/", + "sourceName": "Ranovel", + "options": { + "lang": "English", + "useNewChapterEndpoint": true, + "customJs": "loadedCheerio('img[alt=\"Buy Me a Coffee at ko-fi.com\"]').parent().remove(); loadedCheerio('*').contents().each(function() {if (this.type === 'comment' && this.data.trim() === 'Text Ranovel') {let currentNode = this.next;for (let i = 0; i < 2 && currentNode; i++) {const nextNode = currentNode.next;loadedCheerio(currentNode).remove();currentNode = nextNode;}}});" + } + }, + { + "id": "dragonholic", + "sourceSite": "https://dragonholic.com/", + "sourceName": "Dragonholic", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "etudetranslations", + "sourceSite": "https://etudetranslations.com/", + "sourceName": "Etude Translations", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "massnovel", + "sourceSite": "https://massnovel.fr/", + "sourceName": "MassNovel", + "options": { + "lang": "French", + "useNewChapterEndpoint": true + } + }, + { + "id": "noicetranslations", + "sourceSite": "https://noicetranslations.com/", + "sourceName": "Noice Translations", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "novelninja", + "sourceSite": "https://novelninja.xyz/", + "sourceName": "Novel Ninja", + "options": { + "useNewChapterEndpoint": true + } + }, + { + "id": "markazriwayat", + "sourceSite": "https://markazriwayat.com/", + "sourceName": "Markazriwayat", + "options": { + "lang": "Arabic", + "useNewChapterEndpoint": true + } + }, + { + "id": "lullobox", + "sourceSite": "https://lullobox.com/", + "sourceName": "LulloBox", + "options": { + "lang": "English", + "useNewChapterEndpoint": true + } + }, + { + "id": "boxnovel", + "sourceSite": "https://novelnice.com/", + "sourceName": "BoxNovel", + "options": { + "lang": "English", + "useNewChapterEndpoint": true, + "versionIncrements": 2 + } + } +] diff --git a/plugins/multisrc/madara/template.ts b/plugins/multisrc/madara/template.ts new file mode 100644 index 000000000..ed392b0f1 --- /dev/null +++ b/plugins/multisrc/madara/template.ts @@ -0,0 +1,464 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { Cheerio, CheerioAPI, load as parseHTML } from 'cheerio'; +import { AnyNode } from 'domhandler'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; +import { storage } from '@libs/storage'; + +const includesAny = (str: string, keywords: string[]) => + new RegExp(keywords.join('|')).test(str); + +type MadaraOptions = { + useNewChapterEndpoint?: boolean; + lang?: string; + orderBy?: string; + versionIncrements?: number; + customJs?: string; + hasLocked?: boolean; +}; + +export type MadaraMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: MadaraOptions; + filters?: Filters; +}; + +export class MadaraPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options?: MadaraOptions; + filters?: Filters | undefined; + + hideLocked = storage.get('hideLocked'); + pluginSettings?: Filters; + + constructor(metadata: MadaraMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/madara/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `2.2.${versionIncrements}`; + this.options = metadata.options; + this.filters = metadata.filters; + + if (this.options?.hasLocked) { + this.pluginSettings = { + hideLocked: { + value: '', + label: 'Hide locked chapters', + type: 'Switch', + }, + }; + } + } + + translateDragontea(text: Cheerio<AnyNode>): Cheerio<AnyNode> { + if (this.id !== 'dragontea') return text; + + const $ = parseHTML( + text + .html() + ?.replace('\n', '') + .replace(/<br\s*\/?>/g, '\n') || '', + ); + const reverseAlpha = 'zyxwvutsrqponmlkjihgfedcbaZYXWVUTSRQPONMLKJIHGFEDCBA'; + const forwardAlpha = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + text.html($.html()); + text + .find('*') + .addBack() + .contents() + .filter((_, el) => el.nodeType === 3) + .each((_, el) => { + const $el = $(el); + const translated = $el + .text() + .normalize('NFD') + .split('') + .map(char => { + const base = char.normalize('NFC'); + const idx = forwardAlpha.indexOf(base); + return idx >= 0 + ? reverseAlpha[idx] + char.slice(base.length) + : char; + }) + .join(''); + $el.replaceWith(translated.replace('\n', '<br>')); + }); + + return text; + } + + getHostname(url: string): string { + url = url.split('/')[2]; + const url_parts = url.split('.'); + url_parts.pop(); // remove TLD + return url_parts.join('.'); + } + + async getCheerio(url: string, search: boolean): Promise<CheerioAPI> { + const r = await fetchApi(url); + if (!r.ok && search != true) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const $ = parseHTML(await r.text()); + const title = $('title').text().trim(); + if ( + this.getHostname(url) != this.getHostname(r.url) || + title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...' + ) + throw new Error('Captcha error, please open in webview'); + return $; + } + + parseNovels(loadedCheerio: CheerioAPI): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.manga-title-badges').remove(); + + loadedCheerio('.page-item-detail, .c-tabs-item__content').each( + (index, element) => { + const novelName = loadedCheerio(element) + .find('.post-title') + .text() + .trim(); + const novelUrl = + loadedCheerio(element).find('.post-title').find('a').attr('href') || + ''; + if (!novelName || !novelUrl) return; + const image = loadedCheerio(element).find('img'); + const novelCover = + image.attr('data-src') || + image.attr('src') || + image.attr('data-lazy-srcset') || + defaultCover; + const novel: Plugin.NovelItem = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(/https?:\/\/.*?\//, ''), + }; + novels.push(novel); + }, + ); + + return novels; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/page/' + pageNo + '/?s=&post_type=wp-manga'; + if (!filters) filters = this.filters || {}; + if (showLatestNovels) url += '&m_orderby=latest'; + for (const key in filters) { + if (typeof filters[key].value === 'object') + for (const value of filters[key].value as string[]) + url += `&${key}=${value}`; + else if (filters[key].value) url += `&${key}=${filters[key].value}`; + } + const loadedCheerio = await this.getCheerio(url, pageNo != 1); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + let loadedCheerio = await this.getCheerio(this.site + novelPath, false); + + loadedCheerio('.manga-title-badges, #manga-title span').remove(); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: + loadedCheerio('.post-title h1').text().trim() || + loadedCheerio('#manga-title h1').text().trim() || + loadedCheerio('.manga-title').text().trim() || + '', + }; + + novel.cover = + loadedCheerio('.summary_image > a > img').attr('data-lazy-src') || + loadedCheerio('.summary_image > a > img').attr('data-src') || + loadedCheerio('.summary_image > a > img').attr('src') || + defaultCover; + + loadedCheerio('.post-content_item, .post-content').each(function () { + const detailName = loadedCheerio(this).find('h5').text().trim(); + const detail = + loadedCheerio(this).find('.summary-content') || + loadedCheerio(this).find('.summary_content'); + + switch (detailName) { + case 'Genre(s)': + case 'Genre': + case 'Tags(s)': + case 'Tag(s)': + case 'Tags': + case 'Género(s)': + case 'Kategori': + case 'التصنيفات': + if (novel.genres) + novel.genres += + ', ' + + detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(', '); + else + novel.genres = detail + .find('a') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(', '); + break; + case 'Author(s)': + case 'Author': + case 'Autor(es)': + case 'المؤلف': + case 'المؤلف (ين)': + novel.author = detail.text().trim(); + break; + case 'Status': + case 'Novel': + case 'Estado': + case 'Durum': + novel.status = + detail.text().trim().includes('OnGoing') || + detail.text().trim().includes('مستمرة') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + break; + case 'Artist(s)': + novel.artist = detail.text().trim(); + break; + } + }); + + // Checks for "Madara NovelHub" version + { + if (!novel.genres) + novel.genres = loadedCheerio('.genres-content').text().trim(); + if (!novel.status) + novel.status = loadedCheerio('.manga-status') + .text() + .trim() + .includes('OnGoing') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + if (!novel.author) + novel.author = loadedCheerio('.manga-author a').text().trim(); + if (!novel.rating) + novel.rating = parseFloat( + loadedCheerio('.post-rating span').text().trim(), + ); + } + + if (!novel.author) + novel.author = loadedCheerio('.manga-authors').text().trim(); + + loadedCheerio('div.summary__content .code-block,script,noscript').remove(); + novel.summary = + this.translateDragontea(loadedCheerio('div.summary__content')) + .text() + .trim() || + loadedCheerio('#tab-manga-about').text().trim() || + loadedCheerio('.post-content_item h5:contains("Summary")') + .next() + .find('span') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join('\n\n') + .trim() || + loadedCheerio('.manga-summary p') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join('\n\n') + .trim() || + loadedCheerio('.manga-excerpt p') + .map((i, el) => loadedCheerio(el).text()) + .get() + .join('\n\n') + .trim(); + const chapters: Plugin.ChapterItem[] = []; + let html = ''; + + if (this.options?.useNewChapterEndpoint) { + html = await fetchApi(this.site + novelPath + 'ajax/chapters/', { + method: 'POST', + referrer: this.site + novelPath, + }).then((res: Response) => res.text()); + } else { + const novelId = + loadedCheerio('.rating-post-id').attr('value') || + loadedCheerio('#manga-chapters-holder').attr('data-id') || + ''; + + const formData = new FormData(); + formData.append('action', 'manga_get_chapters'); + formData.append('manga', novelId); + + html = await fetchApi(this.site + 'wp-admin/admin-ajax.php', { + method: 'POST', + body: formData, + }).then((res: Response) => res.text()); + } + + if (html !== '0') { + loadedCheerio = parseHTML(html); + } + + const totalChapters = loadedCheerio('.wp-manga-chapter').length; + loadedCheerio('.wp-manga-chapter').each((chapterIndex, element) => { + let chapterName = loadedCheerio(element).find('a').text().trim(); + const locked = element.attribs['class'].includes('premium-block'); + if (locked) { + chapterName = '🔒 ' + chapterName; + } + + let releaseDate = loadedCheerio(element) + .find('span.chapter-release-date') + .text() + .trim(); + + if (releaseDate) { + releaseDate = this.parseData(releaseDate); + } else { + releaseDate = dayjs().format('LL'); + } + + const chapterUrl = loadedCheerio(element).find('a').attr('href') || ''; + + if (chapterUrl && chapterUrl != '#' && !(locked && this.hideLocked)) { + chapters.push({ + name: chapterName, + path: chapterUrl.replace(/https?:\/\/.*?\//, ''), + releaseTime: releaseDate || null, + chapterNumber: totalChapters - chapterIndex, + }); + } + }); + + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const loadedCheerio = await this.getCheerio(this.site + chapterPath, false); + const chapterText = + loadedCheerio('.text-left') || + loadedCheerio('.text-right') || + loadedCheerio('.entry-content') || + loadedCheerio('.c-blog-post > div > div:nth-child(2)'); + + if (this.options?.customJs) { + try { + // CustomJS HERE + } catch (error) { + console.error('Error executing customJs:', error); + throw error; + } + } + + return this.translateDragontea(chapterText).html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo?: number | undefined, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + + '/page/' + + pageNo + + '/?s=' + + encodeURIComponent(searchTerm) + + '&post_type=wp-manga'; + const loadedCheerio = await this.getCheerio(url, true); + return this.parseNovels(loadedCheerio); + } + + parseData = (date: string) => { + let dayJSDate = dayjs(); // today + const timeAgo = date.match(/\d+/)?.[0] || ''; + const timeAgoInt = parseInt(timeAgo, 10); + + if (!timeAgo) return date; // there is no number! + + if (includesAny(date, ['detik', 'segundo', 'second', 'วินาที'])) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'second'); // go back N seconds + } else if ( + includesAny(date, [ + 'menit', + 'dakika', + 'min', + 'minute', + 'minuto', + 'นาที', + 'دقائق', + ]) + ) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'minute'); // go back N minute + } else if ( + includesAny(date, [ + 'jam', + 'saat', + 'heure', + 'hora', + 'hour', + 'ชั่วโมง', + 'giờ', + 'ore', + 'ساعة', + '小时', + ]) + ) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } else if ( + includesAny(date, [ + 'hari', + 'gün', + 'jour', + 'día', + 'dia', + 'day', + 'วัน', + 'ngày', + 'giorni', + 'أيام', + '天', + ]) + ) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } else if (includesAny(date, ['week', 'semana'])) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'week'); // go back N a week + } else if (includesAny(date, ['month', 'mes'])) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'month'); // go back N months + } else if (includesAny(date, ['year', 'año'])) { + dayJSDate = dayJSDate.subtract(timeAgoInt, 'year'); // go back N years + } else { + if (dayjs(date).format('LL') !== 'Invalid Date') { + return dayjs(date).format('LL'); + } + return date; + } + + return dayJSDate.format('LL'); + }; +} diff --git a/plugins/multisrc/mtlnovel/filters/mtlnovel.json b/plugins/multisrc/mtlnovel/filters/mtlnovel.json new file mode 100644 index 000000000..4404ddea5 --- /dev/null +++ b/plugins/multisrc/mtlnovel/filters/mtlnovel.json @@ -0,0 +1,32 @@ +{ + "order": { + "value": "view", + "label": "Order by", + "options": [ + { "label": "Date", "value": "date" }, + { "label": "Name", "value": "name" }, + { "label": "Rating", "value": "rating" }, + { "label": "View", "value": "view" } + ], + "type": "Picker" + }, + "sort": { + "value": "desc", + "label": "Sort by", + "options": [ + { "label": "Descending", "value": "desc" }, + { "label": "Ascending", "value": "asc" } + ], + "type": "Picker" + }, + "storyStatus": { + "value": "all", + "label": "Status", + "options": [ + { "label": "All", "value": "all" }, + { "label": "Ongoing", "value": "ongoing" }, + { "label": "Complete", "value": "completed" } + ], + "type": "Picker" + } +} \ No newline at end of file diff --git a/plugins/multisrc/mtlnovel/generator.js b/plugins/multisrc/mtlnovel/generator.js new file mode 100644 index 000000000..b058f439b --- /dev/null +++ b/plugins/multisrc/mtlnovel/generator.js @@ -0,0 +1,44 @@ +import list from './sources.json' with { type: 'json' }; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export const generateAll = function () { + list.sort((a, b) => a.id.length - b.id.length); + return list.map(source => { + try { + const filters = JSON.parse( + readFileSync(`${__dirname}/filters/mtlnovel.json`, 'utf-8'), + ); + source.filters = filters; + } catch (e) { + // for the linter + } + console.log( + `[mtlnovel] Generating: ${source.id}`.padEnd(35), + source.filters ? '🔎with filters🔍' : '🚫 no filters 🚫', + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const MTLNovelTemplate = readFileSync(__dirname + '/template.ts', { + encoding: 'utf-8', + }); + + const pluginScript = ` + ${MTLNovelTemplate} + +const plugin = new MTLNovelPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: source.options?.lang || 'English', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/mtlnovel/sources.json b/plugins/multisrc/mtlnovel/sources.json new file mode 100644 index 000000000..8af231047 --- /dev/null +++ b/plugins/multisrc/mtlnovel/sources.json @@ -0,0 +1,50 @@ +[ + { + "id": "mtlnovel", + "sourceSite": "https://www.mtlnovels.com/", + "sourceName": "MTL Novel", + "options": { + "lang": "English" + } + }, + { + "id": "mtlnovel-fr", + "sourceSite": "https://fr.mtlnovels.com/", + "sourceName": "MTL Novel (FR)", + "options": { + "lang": "French" + } + }, + { + "id": "mtlnovel-es", + "sourceSite": "https://es.mtlnovels.com/", + "sourceName": "MTL Novel (ES)", + "options": { + "lang": "Spanish" + } + }, + { + "id": "mtlnovel-id", + "sourceSite": "https://id.mtlnovels.com/", + "sourceName": "MTL Novel (ID)", + "options": { + "lang": "Indonesian" + } + }, + { + "id": "mtlnovel-pt", + "sourceSite": "https://pt.mtlnovels.com/", + "sourceName": "MTL Novel (PT)", + "options": { + "lang": "Portuguese" + } + }, + { + "id": "mtlnovel-ru", + "sourceSite": "https://ru.mtlnovels.com/", + "sourceName": "MTL Novel (RU)", + "options": { + "lang": "Russian" + } + } +] diff --git a/plugins/multisrc/mtlnovel/template.ts b/plugins/multisrc/mtlnovel/template.ts new file mode 100644 index 000000000..3d28ae4a8 --- /dev/null +++ b/plugins/multisrc/mtlnovel/template.ts @@ -0,0 +1,239 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; + +type MTLNovelOptions = { + lang?: string; +}; + +export type MTLNovelMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: MTLNovelOptions; + filters?: Filters; +}; + +export class MTLNovelPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + mainUrl: string; + version: string; + options?: MTLNovelOptions; + filters?: Filters; + + constructor(metadata: MTLNovelMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = 'multisrc/mtlnovel/mtlnovel/icon.png'; + this.site = metadata.sourceSite; + this.mainUrl = 'https://www.mtlnovels.com/'; + this.version = '1.1.3'; + this.options = metadata.options ?? ({} as MTLNovelOptions); + this.filters = metadata.filters satisfies Filters; + } + + async safeFecth( + url: string, + headers: Headers = new Headers(), + ): Promise<Response> { + headers.append('Alt-Used', 'www.mtlnovels.com'); + const r = await fetchApi(url, { headers }); + if (!r.ok) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + return r; + } + + async popularNovels( + page: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = `${this.site}novel-list/?`; + if (filters) { + link += `orderby=${filters.order.value}`; + link += `&order=${filters.sort.value}`; + link += `&status=${filters.storyStatus.value}`; + } + if (showLatestNovels) link += '&m_orderby=date'; + link += `&pg=${page}`; + + const body = await this.safeFecth(link).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.box.wide').each((i, el) => { + const novelName = loadedCheerio(el).find('a.list-title').text().trim(); + let novelCover = loadedCheerio(el).find('amp-img').attr('src'); + if ( + !novelCover || + novelCover == 'https://www.mtlnovel.net/no-image.jpg.webp' + ) + novelCover = defaultCover; + const novelUrl = loadedCheerio(el).find('a.list-title').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.mainUrl, '').replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const headers = new Headers(); + headers.append('Referer', `${this.site}novel-list/`); + + const body = await this.safeFecth(this.site + novelPath, headers).then(r => + r.text(), + ); + + let loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title').text().trim() || 'Untitled', + cover: loadedCheerio('.nov-head > amp-img').attr('src') || defaultCover, + summary: loadedCheerio('div.desc > h2').next().text().trim(), + chapters: [], + }; + + loadedCheerio('.info tr').each((i, el) => { + const infoName = loadedCheerio(el).find('td').eq(0).text().trim(); + const infoValue = loadedCheerio(el).find('td').eq(2).text().trim(); + switch (infoName) { + case 'Genre': + case 'Tags': + case 'Mots Clés': + case 'Género': + case 'Label': + case 'Gênero': + case 'Tag': + case 'Теги': + if (novel.genres) novel.genres += ', ' + infoValue; + else novel.genres = infoValue; + break; + case 'Author': + case 'Auteur': + case 'Autor(a)': + case 'Autor': + case 'Автор': + novel.author = infoValue; + break; + case 'Status': + case 'Statut': + case 'Estado': + case 'Положение дел': + if (infoValue == 'Hiatus') novel.status = NovelStatus.OnHiatus; + else novel.status = infoValue; + break; + } + }); + + const chapterListUrl = this.site + novelPath + 'chapter-list/'; + + const getChapters = async () => { + const listBody = await this.safeFecth(chapterListUrl, headers).then(r => + r.text(), + ); + + loadedCheerio = parseHTML(listBody); + + const chapter: Plugin.ChapterItem[] = []; + + loadedCheerio('div.ch-list') + .find('a.ch-link') + .each((i, el) => { + const chapterName = loadedCheerio(el).text().replace('~ ', ''); + const releaseDate = null; + const chapterUrl = loadedCheerio(el).attr('href'); + if (!chapterUrl) return; + chapter.push({ + path: chapterUrl.replace(this.mainUrl, '').replace(this.site, ''), + name: chapterName, + releaseTime: releaseDate, + }); + }); + return chapter.reverse(); + }; + + novel.chapters = await getChapters(); + + if (novel.genres) { + const genresArray = novel.genres.split(', '); + genresArray.pop(); + novel.genres = genresArray.join(', '); + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await this.safeFecth(this.site + chapterPath).then(r => + r.text(), + ); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.par').html() || ''; + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo !== 1) return []; + const searchUrl = + this.site + + 'wp-admin/admin-ajax.php?action=autosuggest&q=' + + encodeURIComponent(searchTerm); + + const res = await this.safeFecth(searchUrl); + const result = await res.json(); + + const novels: Plugin.NovelItem[] = []; + type SearchEntry = { + title: string; + thumbnail: string; + permalink: string; + }; + result.items[0].results.map((item: SearchEntry) => { + const novelName = item.title.replace(/<\/?strong>/g, ''); + const novelCover = item.thumbnail; + const novelUrl = item.permalink + .replace(this.mainUrl, '') + .replace(this.site, ''); + + const novel = { name: novelName, cover: novelCover, path: novelUrl }; + + novels.push(novel); + }); + + return novels; + } + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + 'Alt-Used': 'www.mtlnovels.com', + }, + }; +} diff --git a/plugins/multisrc/novelcool/generator.js b/plugins/multisrc/novelcool/generator.js new file mode 100644 index 000000000..d9d05c9af --- /dev/null +++ b/plugins/multisrc/novelcool/generator.js @@ -0,0 +1,40 @@ +import list from './sources.json' with { type: 'json' }; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// https://app.novelcool.com/ +const app = { + userAgent: + 'Android/Package:com.zuoyou.novel - Version Name:2.3 - Phone Info:sdk_gphone_x86_64(Android Version:13)', + package_name: 'com.zuoyou.novel', + appId: '202201290625004', + secret: 'c73a8590641781f203660afca1d37ada', +}; + +export const generateAll = function () { + list.sort((a, b) => a.id.length - b.id.length); + return list.map(source => generator(source)); +}; + +const generator = function generator(source) { + const NovelCoolTemplate = readFileSync(__dirname + '/template.ts', { + encoding: 'utf-8', + }); + source.options.app = app; + + const pluginScript = ` + ${NovelCoolTemplate} + +const plugin = new NovelCoolPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: source.options.lang, + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/novelcool/sources.json b/plugins/multisrc/novelcool/sources.json new file mode 100644 index 000000000..ea4aa1503 --- /dev/null +++ b/plugins/multisrc/novelcool/sources.json @@ -0,0 +1,14 @@ +[ + { + "id": "novelcool", + "sourceName": "NovelCool", + "sourceSite": "https://www.novelcool.com", + "options": { "lang": "English", "langCode": "en" } + }, + { + "id": "novelcool-ru", + "sourceName": "NovelCool (RU)", + "sourceSite": "https://ru.novelcool.com", + "options": { "lang": "Russian", "langCode": "ru" } + } +] diff --git a/plugins/multisrc/novelcool/template.ts b/plugins/multisrc/novelcool/template.ts new file mode 100644 index 000000000..cd8547390 --- /dev/null +++ b/plugins/multisrc/novelcool/template.ts @@ -0,0 +1,321 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import dayjs from 'dayjs'; + +type NovelCoolOptions = { + lang: string; + langCode: string; + app: Record<string, string>; +}; + +export type NovelCoolMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options: NovelCoolOptions; +}; + +export class NovelCoolPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + mainUrl: string; + version: string; + options: NovelCoolOptions; + filters: Filters; + + constructor(metadata: NovelCoolMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.site = metadata.sourceSite; + this.icon = 'multisrc/novelcool/novelcool/icon.png'; + this.mainUrl = 'https://api.novelcool.com'; + this.version = '1.0.0'; + this.options = metadata.options ?? ({} as NovelCoolOptions); + this.filters = { + sortby: { + label: 'Order by', + value: 'hot', + options: [ + { label: 'Hottest', value: 'hot' }, + { label: 'Latest', value: 'latest' }, + { label: 'New Books', value: 'new_book' }, + ], + type: FilterTypes.Picker, + }, + }; + } + + async popularNovels( + page: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const sortby = showLatestNovels + ? 'latest' + : filters?.sortby?.value || 'hot'; + + const { list }: { list: Novel[] } = await fetchApi( + this.mainUrl + '/elite/' + sortby, + { + headers: { + 'User-Agent': this.options.app.userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'post', + body: new URLSearchParams({ + appId: this.options.app.appId, + secret: this.options.app.secret, + package_name: this.options.app.package_name, + lc_type: 'novel', + lang: this.options.langCode, + page: page.toString(), + page_size: '20', + }).toString(), + }, + ).then((res: Response) => res.json()); + const novels: Plugin.NovelItem[] = []; + + list.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.cover, + path: novel.visit_path + '?id=' + novel.id, + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const book_id = novelPath.split('?id=')[1]; + const { info }: { info: Novel } = await fetchApi( + this.mainUrl + '/book/info/', + { + headers: { + 'User-Agent': this.options.app.userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'post', + body: new URLSearchParams({ + book_id, + appId: this.options.app.appId, + secret: this.options.app.secret, + package_name: this.options.app.package_name, + lang: this.options.langCode, + }).toString(), + }, + ).then((res: Response) => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: info.name, + cover: info.cover, + genres: info.category_list.join(','), + summary: info.intro, + author: info.author, + artist: info.artist, + status: + info.completed === 'YES' ? NovelStatus.Completed : NovelStatus.Ongoing, + rating: (info.rate_star && parseFloat(info.rate_star)) || undefined, + }; + const chapters: Plugin.ChapterItem[] = []; + + const { list }: { list: ChapterNext[] } = await fetchApi( + this.mainUrl + '/chapter/book_list/', + { + headers: { + 'User-Agent': this.options.app.userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'post', + body: new URLSearchParams({ + book_id, + appId: this.options.app.appId, + secret: this.options.app.secret, + package_name: this.options.app.package_name, + lang: this.options.langCode, + }).toString(), + }, + ).then((res: Response) => res.json()); + + list.forEach(chapter => { + if (!chapter.is_locked) { + chapters.push({ + name: chapter.title, + path: chapter.book_id + '/' + chapter.id, + releaseTime: dayjs(parseInt(chapter.last_modify, 10) * 1000).format( + 'LLL', + ), + chapterNumber: parseInt(chapter.order_id, 10), + }); + } + }); + + novel.chapters = chapters.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [, chapter_id] = chapterPath.split('/'); + const { info }: { info: Chapter } = await fetchApi( + this.mainUrl + '/chapter/info/', + { + headers: { + 'User-Agent': this.options.app.userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'post', + body: new URLSearchParams({ + chapter_id, + appId: this.options.app.appId, + secret: this.options.app.secret, + package_name: this.options.app.package_name, + lang: this.options.langCode, + }).toString(), + }, + ).then((res: Response) => res.json()); + + return info.content; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const { list }: { list: Novel[] } = await fetchApi( + this.mainUrl + '/book/search/', + { + headers: { + 'User-Agent': this.options.app.userAgent, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + method: 'post', + body: new URLSearchParams({ + appId: this.options.app.appId, + secret: this.options.app.secret, + package_name: this.options.app.package_name, + keyword: searchTerm, + lc_type: 'novel', + lang: this.options.langCode, + page: pageNo.toString(), + page_size: '20', + }).toString(), + }, + ).then((res: Response) => res.json()); + const novels: Plugin.NovelItem[] = []; + + list.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.cover, + path: novel.visit_path + '?id=' + novel.id, + }), + ); + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/novel/' : '/chapter/') + path.split('?id=')[0]; +} + +type Novel = { + id: string; + lang: string; + url: string; + visit_path: string; + name: string; + alternative: string; + publish_year: string; + warning: string; + char_index: string; + author: string; + artist: string; + intro: string; + tags?: string; + category: string; + completed: 'YES' | 'NO'; + last_chapter_id: string; + last_chapter_title: string; + all_views: string; + rate_star: string; + rate_mark: string; + rate_num: string; + follow_num: string; + pic_num: string; + make_time: Date; + copy_limit: string; + copyright: string; + user_id: string; + show_it: 'ALLOW'; + type: string; + elite: string; + book_id: string; + is_novel: string; + is_new: string; + is_hot: string; + cover: string; + og_url?: string; + no: string; + last_url?: string; + category_str: string; + category_list: string[]; + // star_list: any[]; + int_mark: string; + time: string; + show_ads: string; + first_chapter_id?: string; + first_chapter_type?: number; + is_following: boolean; + copyright_limit: boolean; + share_title?: string; +}; + +type Chapter = { + id: string; + user_id: string; + vol_id: string; + lang: string; + order_id: string; + book_id: string; + title: string; + type: number; + url: string; + content: string; + content_size: string; + fixed: string; + group_name: string; + last_modify: string; + add_time: Date; + prev: ChapterNext; + next: ChapterNext; + blank: boolean; + o_url: string; + is_locked: boolean; + show_url: boolean; + is_following: boolean; + share_title: string; +}; + +type ChapterNext = { + id: string; + book_id: string; + type: number; + lang: string; + vol_id: string; + order_id: string; + title: string; + last_modify: string; + url: string; + no: string; + is_locked: boolean; + is_new: boolean; + tf_time: string; + url_pre?: string; +}; diff --git a/plugins/multisrc/ranobes/generator.js b/plugins/multisrc/ranobes/generator.js new file mode 100644 index 000000000..07229b7e3 --- /dev/null +++ b/plugins/multisrc/ranobes/generator.js @@ -0,0 +1,31 @@ +import list from './sources.json' with { type: 'json' }; +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(metadata => { + console.log(`[ranobes]: Generating`, metadata.id); + return generator(metadata); + }); +}; + +const generator = function generator(metadata) { + const RanobesTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` + ${RanobesTemplate} +const plugin = new RanobesPlugin(${JSON.stringify(metadata)}); +export default plugin; + `.trim(); + + return { + lang: metadata.options.lang, + filename: metadata.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/ranobes/sources.json b/plugins/multisrc/ranobes/sources.json new file mode 100644 index 000000000..29f2d9adc --- /dev/null +++ b/plugins/multisrc/ranobes/sources.json @@ -0,0 +1,20 @@ +[ + { + "id": "ranobes", + "sourceSite": "https://ranobes.top", + "sourceName": "Ranobes", + "options": { + "lang": "English", + "path": "novels" + } + }, + { + "id": "ranobes-ru", + "sourceSite": "https://ranobes.com", + "sourceName": "Ranobes (RU)", + "options": { + "lang": "Russian", + "path": "ranobe" + } + } +] \ No newline at end of file diff --git a/plugins/multisrc/ranobes/template.ts b/plugins/multisrc/ranobes/template.ts new file mode 100644 index 000000000..13d2613f8 --- /dev/null +++ b/plugins/multisrc/ranobes/template.ts @@ -0,0 +1,471 @@ +import { Parser } from 'htmlparser2'; +import { fetchApi, FetchInit } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; + +type RanobesOptions = { + lang?: string; + path: string; +}; + +export type RanobesMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: RanobesOptions; +}; + +export class RanobesPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options: RanobesOptions; + + constructor(metadata: RanobesMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = 'multisrc/ranobes/ranobes/icon.png'; + this.site = metadata.sourceSite; + this.version = '2.0.2'; + this.options = metadata.options as RanobesOptions; + } + + async safeFecth(url: string, init?: FetchInit): Promise<string> { + const r = await fetchApi(url, init); + if (!r.ok) + throw new Error( + 'Could not reach site (' + r.status + ') try to open in webview.', + ); + const data = await r.text(); + const title = data.match(/<title>(.*?)<\/title>/)?.[1]?.trim(); + + if ( + title && + (title == 'Bot Verification' || + title == 'You are being redirected...' || + title == 'Un instant...' || + title == 'Just a moment...' || + title == 'Redirecting...') + ) + throw new Error('Captcha error, please open in webview'); + + return data; + } + + parseNovels(html: string) { + const novels: Plugin.NovelItem[] = []; + let tempNovel = {} as Plugin.NovelItem; + tempNovel.name = ''; + const baseUrl = this.site; + let isParsingNovel = false; + let isTitleTag = false; + let isNovelName = false; + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class']?.includes('short-cont')) { + isParsingNovel = true; + } + if (isParsingNovel) { + if (name === 'h2' && attribs['class']?.includes('title')) { + isTitleTag = true; + } + if (isTitleTag && name === 'a') { + tempNovel.path = attribs['href'].slice(baseUrl.length); + isNovelName = true; + } + if (name === 'figure') { + tempNovel.cover = attribs['style'].replace( + /.*url\((.*?)\)./g, + '$1', + ); + } + if (tempNovel.path && tempNovel.cover) { + novels.push(tempNovel); + tempNovel = {} as Plugin.NovelItem; + tempNovel.name = ''; + } + } + }, + ontext(data) { + if (isNovelName) { + tempNovel.name += data; + } + }, + onclosetag(name) { + if (name === 'h2') { + isNovelName = false; + isTitleTag = false; + } + if (name === 'figure') { + isParsingNovel = false; + } + }, + }); + parser.write(html); + parser.end(); + return novels; + } + + parseChapters(data: { chapters: ChapterEntry[] }) { + const chapter: Plugin.ChapterItem[] = []; + data.chapters.map((item: ChapterEntry) => { + chapter.push({ + name: item.title, + releaseTime: new Date(item.date).toISOString(), + path: item.link.slice(this.site.length), + }); + }); + return chapter.reverse(); + } + + parseDate = (date: string) => { + const now = new Date(); + if (!date) return now.toISOString(); + if (this.id === 'ranobes-ru') { + if (date.includes(' в ')) return date.replace(' в ', ' г., '); + + const [when, time] = date.split(', '); + if (!time) return now.toISOString(); + const [h, m] = time.split(':'); + + switch (when) { + case 'Сегодня': + now.setHours(parseInt(h, 10)); + now.setMinutes(parseInt(m, 10)); + break; + case 'Вчера': + now.setDate(now.getDate() - 1); + now.setHours(parseInt(h, 10)); + now.setMinutes(parseInt(m, 10)); + break; + default: + return now.toISOString(); + } + } else { + const [num, xz, ago] = date.split(' '); + if (ago !== 'ago') return now.toISOString(); + + switch (xz) { + case 'minutes': + now.setMinutes(parseInt(num, 10)); + break; + case 'hour': + case 'hours': + now.setHours(parseInt(num, 10)); + break; + case 'day': + case 'days': + now.setDate(now.getDate() - parseInt(num, 10)); + break; + case 'month': + case 'months': + now.setMonth(now.getMonth() - parseInt(num, 10)); + break; + case 'year': + case 'years': + now.setFullYear(now.getFullYear() - parseInt(num, 10)); + break; + default: + return now.toISOString(); + } + } + return now.toISOString(); + }; + + async popularNovels(page: number): Promise<Plugin.NovelItem[]> { + const link = `${this.site}/${this.options.path}/page/${page}/`; + const body = await this.safeFecth(link); + return this.parseNovels(body); + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const baseUrl = this.site; + const html = await this.safeFecth(baseUrl + novelPath); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: '', + summary: '', + chapters: [], + totalPages: 1, + }; + let isCover = false; + let isAuthor = false; + let isSummary = false; + let isStatus = false; + let isStatusText = false; + let isGenres = false; + let isGenresText = false; + let isMaxChapters = false; + let isChapter = false; + let isChapterTitle = false; + let isChapterDate = false; + const genreArray: string[] = []; + const chapters: Plugin.ChapterItem[] = []; + let tempchapter: Plugin.ChapterItem = {}; + let maxChapters = 0; + const fixDate = this.parseDate; + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class'] === 'poster') { + isCover = true; + } + if (isCover && name === 'img') { + novel.name = attribs['alt']; + novel.cover = baseUrl + attribs['src']; + } + if ( + (name === 'div' && + attribs['class'] === 'moreless cont-text showcont-h') || + (attribs['class'] === 'cont-text showcont-h' && + attribs['itemprop'] === 'description') + ) { + isSummary = true; + } + if ( + name === 'li' && + attribs['title'] && + (attribs['title'].includes('Original status') || + attribs['title'].includes('Статус оригинала')) + ) { + isStatus = true; + } + if (name === 'a' && attribs['rel'] === 'chapter') { + isChapter = true; + tempchapter.path = attribs['href'].replace(baseUrl, ''); + } + if ( + isChapter && + name === 'span' && + attribs['class'] === 'title ellipses' + ) { + isChapterTitle = true; + } + if (isChapter && name === 'span' && attribs['class'] === 'grey') { + isChapterDate = true; + } + if ( + name === 'li' && + (attribs['title'] == + 'Glossary + illustrations + division of chapters, etc.' || + attribs['title'] === + 'Глоссарий + иллюстраций + разделение глав и т.д.') + ) { + isMaxChapters = true; + } + }, + onopentagname(name) { + if (isSummary && name === 'br') { + novel.summary += '\n'; + } + if (isStatus && name === 'a') { + isStatusText = true; + } + if (isGenres && name === 'a') { + isGenresText = true; + } + }, + onattribute(name, value) { + if (name === 'itemprop' && value === 'creator') { + isAuthor = true; + } + if (name === 'id' && value === 'mc-fs-genre') { + isGenres = true; + } + }, + ontext(data) { + if (isAuthor) { + novel.author = data; + } + if (isSummary) { + novel.summary += data.trim(); + } + if (isStatusText) { + novel.status = + data === 'Ongoing' || data == 'В процессе' + ? NovelStatus.Ongoing + : NovelStatus.Completed; + } + if (isGenresText) { + genreArray.push(data); + } + if (isMaxChapters) { + const isNumber = data.replace(/\D/g, ''); + if (isNumber) { + maxChapters = parseInt(isNumber, 10); + } + } + if (isChapter) { + if (isChapterTitle) tempchapter.name = data.trim(); + if (isChapterDate) tempchapter.releaseTime = fixDate(data.trim()); + } + }, + onclosetag(name) { + if (name === 'a') { + isCover = false; + isAuthor = false; + isStatusText = false; + isGenresText = false; + isStatus = false; + } + if (name === 'div') { + isSummary = false; + isGenres = false; + } + if (name === 'li') { + isMaxChapters = false; + } + if (name === 'a') { + isChapter = false; + if (tempchapter.name) { + chapters.push({ ...tempchapter, page: '1' }); + tempchapter = {}; + } + } + if (name === 'span') { + if (isChapterTitle) isChapterTitle = false; + if (isChapterDate) isChapterDate = false; + } + }, + }); + parser.write(html); + parser.end(); + novel.genres = genreArray.join(', '); + novel.totalPages = Math.ceil((maxChapters || 1) / 25); + novel.chapters = chapters; + + if (novel.chapters[0].path) { + novel.latestChapter = novel.chapters[0]; + } + + return novel; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const pagePath = + this.id == 'ranobes' + ? novelPath.split('-')[0] + : '/' + novelPath.split('-').slice(1).join('-').split('.')[0]; + const firstUrl = + this.site + '/chapters' + pagePath.replace(this.options.path + '/', ''); + const pageBody = await this.safeFecth(firstUrl + '/page/' + page); + + const baseUrl = this.site; + let isScript = false; + let isChapter = false; + let isChapterInfo = false; + let isChapterDate = false; + + let chapters: Plugin.ChapterItem[] = []; + let tempchapter: Plugin.ChapterItem = {}; + const fixDate = this.parseDate; + + let dataJson: { + pages_count: string; + chapters: ChapterEntry[]; + } = { pages_count: '', chapters: [] }; + + const parser = new Parser({ + onopentag(name, attribs) { + if (name === 'div' && attribs['class'] === 'cat_block cat_line') { + isChapter = true; + } + if (isChapter && name === 'a' && attribs['title'] && attribs['href']) { + tempchapter.name = attribs['title']; + tempchapter.path = attribs['href'].replace(baseUrl, ''); + } + if (name === 'span' && attribs['class'] === 'grey small') { + isChapterInfo = true; + } + if (name === 'small' && isChapterInfo) { + isChapterDate = true; + } + }, + ontext(data) { + if (isChapterDate) tempchapter.releaseTime = fixDate(data.trim()); + if (isScript) { + if (data.includes('window.__DATA__ =')) { + dataJson = JSON.parse(data.replace('window.__DATA__ =', '')); + } + } + }, + onclosetag(name) { + if (name === 'a' && tempchapter.name) { + chapters.push(tempchapter); + tempchapter = {}; + } + if (name === 'div') { + isChapter = false; + } + if (name === 'span') { + isChapterInfo = false; + } + if (name === 'small') { + isChapterDate = false; + } + if (name === 'main') { + isScript = true; + } + if (name === 'script') { + isScript = false; + } + }, + }); + parser.write(pageBody); + parser.end(); + + if (dataJson.chapters?.length) { + chapters = this.parseChapters(dataJson); + } + + return { + chapters, + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const html = await this.safeFecth(this.site + chapterPath); + + const indexA = html.indexOf('<div class="text" id="arrticle">'); + const indexB = html.indexOf('<div class="category grey ellipses">', indexA); + + const chapterText = html.substring(indexA, indexB); + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + let html; + if (this.id === 'ranobes-ru') { + html = await this.safeFecth(this.site + '/index.php?do=search', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/', + }, + method: 'POST', + body: new URLSearchParams({ + do: 'search', + subaction: 'search', + search_start: page.toString(), + story: searchTerm, + }).toString(), + }); + } else { + const link = `${this.site}/search/${searchTerm}/page/${page}`; + html = await this.safeFecth(link); + } + return this.parseNovels(html); + } +} + +type ChapterEntry = { + id: number; + title: string; + date: string; + link: string; +}; diff --git a/plugins/multisrc/readnovelfull/README.md b/plugins/multisrc/readnovelfull/README.md new file mode 100644 index 000000000..7e0e7d64f --- /dev/null +++ b/plugins/multisrc/readnovelfull/README.md @@ -0,0 +1,43 @@ +# Readnovelfull multisrc generator + +## Compatiblity + +### This generator is brought over from the madara generator one folder away + +it should(!) work for all sites that uses this theme. If not, edit. + +## Add a new source + +### sources.json + +To add a new source you need to add it to sources.json: + +- id: the id of the source (something unique) +- sourceName: the name of the source (you can use the value of "name" in "https://site.com/wp-json/" if it exists) +- sourceSite: the site url +- options: the options of the source + + - lang?: the language of the source (default: "English") (check that the language + exists in the languages (check folder names in "plugins/")) + - versionIncrements?: needs to be updated everytime the site url is updated + - latestPage: the href value using this CSS selector `a[title="Latest Release"]` + - searchPage: path for search + - chapterListing?: if chapters are obtained from an ajax endpoint different from what is default. + - chapterParam?: same with chapterListing, but its the key for the form body. default: novelId + - pageParam?: if a site uses something other than "page" for accessing pages + + // All the following is for when if a page uses a main path for querying novels and just adds params to it to get different outputs (see urls in lightnovelplus) + + - novelListing?: Main path for browsing novels + - typeParam?: Field for entering the type of novelListing (eg: latest, hot, completed, etc), default: type + - genreParam?: Field for entering type of novelListing dedicated for genres (since param websites dont use the path change), default: category_novel + - genreKey?: Field for entering key representing the genre within the genreParam, default: id (eg: ?type=category_novel&id={value}) + - langParam?: Field for entering the key representing language(?), do not understand reason for this param honestly, but recreate normal website behaviour as much as possible, default: none. + - urlLangCode?: the value for langParam, default: none. eg: 'en' + +### filters + +To add filters to a source you need to run the script "get_filters.js" \ +(`npx node plugins/multisrc/readnovelfull/get_filters.js` +(if you are at the root of the project) (and you have ran "npm install" before)) +and follow the instructions (url is easier and faster but sometimes it doesn't work) diff --git a/plugins/multisrc/readnovelfull/filters/FWN.com.json b/plugins/multisrc/readnovelfull/filters/FWN.com.json new file mode 100644 index 000000000..5ae7d4930 --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/FWN.com.json @@ -0,0 +1,78 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Type", + "value": "sort/most-popular", + "options": [ + { + "label": "All", + "value": "sort/latest-release" + }, + { + "label": "Chinese Novel", + "value": "sort/latest-release/chinese-novel" + }, + { + "label": "Korean Novel", + "value": "sort/latest-release/korean-novel" + }, + { + "label": "Japanese Novel", + "value": "sort/latest-release/japanese-novel" + }, + { + "label": "English Novel", + "value": "sort/latest-release/english-novel" + }, + { + "label": "Most Popular", + "value": "sort/most-popular" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { "label": "Action", "value": "genre/Action" }, + { "label": "Adult", "value": "genre/Adult" }, + { "label": "Adventure", "value": "genre/Adventure" }, + { "label": "Comedy", "value": "genre/Comedy" }, + { "label": "Drama", "value": "genre/Drama" }, + { "label": "Eastern", "value": "genre/Eastern" }, + { "label": "Ecchi", "value": "genre/Ecchi" }, + { "label": "Fantasy", "value": "genre/Fantasy" }, + { "label": "Game", "value": "genre/Game" }, + { "label": "Gender Bender", "value": "genre/Gender+Bender" }, + { "label": "Harem", "value": "genre/Harem" }, + { "label": "Historical", "value": "genre/Historical" }, + { "label": "Horror", "value": "genre/Horror" }, + { "label": "Josei", "value": "genre/Josei" }, + { "label": "Martial Arts", "value": "genre/Martial+Arts" }, + { "label": "Mature", "value": "genre/Mature" }, + { "label": "Mecha", "value": "genre/Mecha" }, + { "label": "Mystery", "value": "genre/Mystery" }, + { "label": "Psychological", "value": "genre/Psychological" }, + { "label": "Reincarnation", "value": "genre/Reincarnation" }, + { "label": "Romance", "value": "genre/Romance" }, + { "label": "School Life", "value": "genre/School+Life" }, + { "label": "Sci-fi", "value": "genre/Sci-fi" }, + { "label": "Seinen", "value": "genre/Seinen" }, + { "label": "Shoujo", "value": "genre/Shoujo" }, + { "label": "Shounen Ai", "value": "genre/Shounen+Ai" }, + { "label": "Shounen", "value": "genre/Shounen" }, + { "label": "Slice of Life", "value": "genre/Slice+of+Life" }, + { "label": "Smut", "value": "genre/Smut" }, + { "label": "Sports", "value": "genre/Sports" }, + { "label": "Supernatural", "value": "genre/Supernatural" }, + { "label": "Tragedy", "value": "genre/Tragedy" }, + { "label": "Wuxia", "value": "genre/Wuxia" }, + { "label": "Xianxia", "value": "genre/Xianxia" }, + { "label": "Xuanhuan", "value": "genre/Xuanhuan" }, + { "label": "Yaoi", "value": "genre/Yaoi" } + ] + } + } +} diff --git a/plugins/multisrc/readnovelfull/filters/allnovel.json b/plugins/multisrc/readnovelfull/filters/allnovel.json new file mode 100644 index 000000000..a1c640b83 --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/allnovel.json @@ -0,0 +1,174 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "most-popular", + "options": [ + { + "label": "Hot Novel", + "value": "hot-novel" + }, + { + "label": "Completed Novel", + "value": "completed-novel" + }, + { + "label": "Most Popular", + "value": "most-popular" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { + "label": "Shounen", + "value": "genre/Shounen" + }, + { + "label": "Harem", + "value": "genre/Harem" + }, + { + "label": "Comedy", + "value": "genre/Comedy" + }, + { + "label": "Martial Arts", + "value": "genre/Martial+Arts" + }, + { + "label": "School Life", + "value": "genre/School+Life" + }, + { + "label": "Mystery", + "value": "genre/Mystery" + }, + { + "label": "Shoujo", + "value": "genre/Shoujo" + }, + { + "label": "Romance", + "value": "genre/Romance" + }, + { + "label": "Sci-fi", + "value": "genre/Sci-fi" + }, + { + "label": "Gender Bender", + "value": "genre/Gender+Bender" + }, + { + "label": "Mature", + "value": "genre/Mature" + }, + { + "label": "Fantasy", + "value": "genre/Fantasy" + }, + { + "label": "Horror", + "value": "genre/Horror" + }, + { + "label": "Drama", + "value": "genre/Drama" + }, + { + "label": "Tragedy", + "value": "genre/Tragedy" + }, + { + "label": "Supernatural", + "value": "genre/Supernatural" + }, + { + "label": "Ecchi", + "value": "genre/Ecchi" + }, + { + "label": "Xuanhuan", + "value": "genre/Xuanhuan" + }, + { + "label": "Adventure", + "value": "genre/Adventure" + }, + { + "label": "Action", + "value": "genre/Action" + }, + { + "label": "Psychological", + "value": "genre/Psychological" + }, + { + "label": "Xianxia", + "value": "genre/Xianxia" + }, + { + "label": "Wuxia", + "value": "genre/Wuxia" + }, + { + "label": "Historical", + "value": "genre/Historical" + }, + { + "label": "Slice of Life", + "value": "genre/Slice+of+Life" + }, + { + "label": "Seinen", + "value": "genre/Seinen" + }, + { + "label": "Lolicon", + "value": "genre/Lolicon" + }, + { + "label": "Adult", + "value": "genre/Adult" + }, + { + "label": "Josei", + "value": "genre/Josei" + }, + { + "label": "Sports", + "value": "genre/Sports" + }, + { + "label": "Smut", + "value": "genre/Smut" + }, + { + "label": "Mecha", + "value": "genre/Mecha" + }, + { + "label": "Yaoi", + "value": "genre/Yaoi" + }, + { + "label": "Shounen Ai", + "value": "genre/Shounen+Ai" + }, + { + "label": "History", + "value": "genre/History" + }, + { + "label": "Reincarnation", + "value": "genre/Reincarnation" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/filters/anf.net.json b/plugins/multisrc/readnovelfull/filters/anf.net.json new file mode 100644 index 000000000..242e1d06b --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/anf.net.json @@ -0,0 +1,174 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "most-popular", + "options": [ + { + "label": "Hot Novel", + "value": "hot-novel" + }, + { + "label": "Completed Novel", + "value": "completed-novel" + }, + { + "label": "Most Popular", + "value": "most-popular" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { + "label": "Shounen", + "value": "genre/Shounen" + }, + { + "label": "Harem", + "value": "genre/Harem" + }, + { + "label": "Comedy", + "value": "genre/Comedy" + }, + { + "label": "Martial Arts", + "value": "genre/Martial+Arts" + }, + { + "label": "School Life", + "value": "genre/School+Life" + }, + { + "label": "Mystery", + "value": "genre/Mystery" + }, + { + "label": "Shoujo", + "value": "genre/Shoujo" + }, + { + "label": "Romance", + "value": "genre/Romance" + }, + { + "label": "Sci-fi", + "value": "genre/Sci-fi" + }, + { + "label": "Gender Bender", + "value": "genre/Gender+Bender" + }, + { + "label": "Mature", + "value": "genre/Mature" + }, + { + "label": "Fantasy", + "value": "genre/Fantasy" + }, + { + "label": "Horror", + "value": "genre/Horror" + }, + { + "label": "Drama", + "value": "genre/Drama" + }, + { + "label": "Tragedy", + "value": "genre/Tragedy" + }, + { + "label": "Supernatural", + "value": "genre/Supernatural" + }, + { + "label": "Ecchi", + "value": "genre/Ecchi" + }, + { + "label": "Xuanhuan", + "value": "genre/Xuanhuan" + }, + { + "label": "Adventure", + "value": "genre/Adventure" + }, + { + "label": "Action", + "value": "genre/Action" + }, + { + "label": "Psychological", + "value": "genre/Psychological" + }, + { + "label": "Xianxia", + "value": "genre/Xianxia" + }, + { + "label": "Wuxia", + "value": "genre/Wuxia" + }, + { + "label": "Historical", + "value": "genre/Historical" + }, + { + "label": "Slice of Life", + "value": "genre/Slice+of+Life" + }, + { + "label": "Seinen", + "value": "genre/Seinen" + }, + { + "label": "Lolicon", + "value": "genre/Lolicon" + }, + { + "label": "Adult", + "value": "genre/Adult" + }, + { + "label": "Josei", + "value": "genre/Josei" + }, + { + "label": "Sports", + "value": "genre/Sports" + }, + { + "label": "Smut", + "value": "genre/Smut" + }, + { + "label": "Mecha", + "value": "genre/Mecha" + }, + { + "label": "Yaoi", + "value": "genre/Yaoi" + }, + { + "label": "Shounen Ai", + "value": "genre/Shounen+Ai" + }, + { + "label": "Magical Realism", + "value": "genre/Magical+Realism" + }, + { + "label": "Video Games", + "value": "genre/Video+Games" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/filters/libread.json b/plugins/multisrc/readnovelfull/filters/libread.json new file mode 100644 index 000000000..5ae7d4930 --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/libread.json @@ -0,0 +1,78 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Type", + "value": "sort/most-popular", + "options": [ + { + "label": "All", + "value": "sort/latest-release" + }, + { + "label": "Chinese Novel", + "value": "sort/latest-release/chinese-novel" + }, + { + "label": "Korean Novel", + "value": "sort/latest-release/korean-novel" + }, + { + "label": "Japanese Novel", + "value": "sort/latest-release/japanese-novel" + }, + { + "label": "English Novel", + "value": "sort/latest-release/english-novel" + }, + { + "label": "Most Popular", + "value": "sort/most-popular" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { "label": "Action", "value": "genre/Action" }, + { "label": "Adult", "value": "genre/Adult" }, + { "label": "Adventure", "value": "genre/Adventure" }, + { "label": "Comedy", "value": "genre/Comedy" }, + { "label": "Drama", "value": "genre/Drama" }, + { "label": "Eastern", "value": "genre/Eastern" }, + { "label": "Ecchi", "value": "genre/Ecchi" }, + { "label": "Fantasy", "value": "genre/Fantasy" }, + { "label": "Game", "value": "genre/Game" }, + { "label": "Gender Bender", "value": "genre/Gender+Bender" }, + { "label": "Harem", "value": "genre/Harem" }, + { "label": "Historical", "value": "genre/Historical" }, + { "label": "Horror", "value": "genre/Horror" }, + { "label": "Josei", "value": "genre/Josei" }, + { "label": "Martial Arts", "value": "genre/Martial+Arts" }, + { "label": "Mature", "value": "genre/Mature" }, + { "label": "Mecha", "value": "genre/Mecha" }, + { "label": "Mystery", "value": "genre/Mystery" }, + { "label": "Psychological", "value": "genre/Psychological" }, + { "label": "Reincarnation", "value": "genre/Reincarnation" }, + { "label": "Romance", "value": "genre/Romance" }, + { "label": "School Life", "value": "genre/School+Life" }, + { "label": "Sci-fi", "value": "genre/Sci-fi" }, + { "label": "Seinen", "value": "genre/Seinen" }, + { "label": "Shoujo", "value": "genre/Shoujo" }, + { "label": "Shounen Ai", "value": "genre/Shounen+Ai" }, + { "label": "Shounen", "value": "genre/Shounen" }, + { "label": "Slice of Life", "value": "genre/Slice+of+Life" }, + { "label": "Smut", "value": "genre/Smut" }, + { "label": "Sports", "value": "genre/Sports" }, + { "label": "Supernatural", "value": "genre/Supernatural" }, + { "label": "Tragedy", "value": "genre/Tragedy" }, + { "label": "Wuxia", "value": "genre/Wuxia" }, + { "label": "Xianxia", "value": "genre/Xianxia" }, + { "label": "Xuanhuan", "value": "genre/Xuanhuan" }, + { "label": "Yaoi", "value": "genre/Yaoi" } + ] + } + } +} diff --git a/plugins/multisrc/readnovelfull/filters/lightnovelplus.json b/plugins/multisrc/readnovelfull/filters/lightnovelplus.json new file mode 100644 index 000000000..54795a6db --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/lightnovelplus.json @@ -0,0 +1,130 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "hot_novel", + "options": [ + { + "label": "Hot Novel", + "value": "hot_novel" + }, + { + "label": "Novel Completed", + "value": "completed_novel" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { + "label": "Fantasy", + "value": "60" + }, + { + "label": "Action", + "value": "132" + }, + { + "label": "Sci-fi", + "value": "61" + }, + { + "label": "Romance", + "value": "59" + }, + { + "label": "Adventure", + "value": "62" + }, + { + "label": "Xuanhuan", + "value": "64" + }, + { + "label": "Modern", + "value": "66" + }, + { + "label": "Mystery", + "value": "63" + }, + { + "label": "Romance", + "value": "68" + }, + { + "label": "Fantasy", + "value": "70" + }, + { + "label": "Historical", + "value": "74" + }, + { + "label": "Sci-fi", + "value": "75" + }, + { + "label": "Xuanhuan", + "value": "76" + }, + { + "label": "Mystery", + "value": "77" + }, + { + "label": "Adventure", + "value": "116" + }, + { + "label": "LGBT+", + "value": "182" + }, + { + "label": "Fantasy Romance", + "value": "134" + }, + { + "label": "Video Games", + "value": "243" + }, + { + "label": "Sci-fi Romance", + "value": "252" + }, + { + "label": "Historical Romance", + "value": "256" + }, + { + "label": "Magical Realism", + "value": "331" + }, + { + "label": "Eastern Fantasy", + "value": "334" + }, + { + "label": "Contemporary Romance", + "value": "344" + }, + { + "label": "Games", + "value": "503" + }, + { + "label": "Urban", + "value": "504" + }, + { + "label": "Harem", + "value": "517" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/filters/novelbin.json b/plugins/multisrc/readnovelfull/filters/novelbin.json new file mode 100644 index 000000000..c6ff03f9b --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/novelbin.json @@ -0,0 +1,263 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "sort/top-view-novel", + "options": [ + { + "label": "Hot Novel", + "value": "sort/top-hot-novel" + }, + { + "label": "Completed Novel", + "value": "sort/completed" + }, + { + "label": "Most Popular", + "value": "sort/top-view-novel" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre (Cancels out 'Novel Listing')", + "value": "", + "options": [ + { + "label": "Action", + "value": "genre/action" + }, + { + "label": "Adventure", + "value": "genre/adventure" + }, + { + "label": "Anime & comics", + "value": "genre/anime-&-comics" + }, + { + "label": "Comedy", + "value": "genre/comedy" + }, + { + "label": "Drama", + "value": "genre/drama" + }, + { + "label": "Eastern", + "value": "genre/eastern" + }, + { + "label": "Fan-fiction", + "value": "genre/fan-fiction" + }, + { + "label": "Fanfiction", + "value": "genre/fanfiction" + }, + { + "label": "Fantasy", + "value": "genre/fantasy" + }, + { + "label": "Game", + "value": "genre/game" + }, + { + "label": "Games", + "value": "genre/games" + }, + { + "label": "Gender bender", + "value": "genre/gender-bender" + }, + { + "label": "General", + "value": "genre/general" + }, + { + "label": "Harem", + "value": "genre/harem" + }, + { + "label": "Historical", + "value": "genre/historical" + }, + { + "label": "Horror", + "value": "genre/horror" + }, + { + "label": "Isekai", + "value": "genre/isekai" + }, + { + "label": "Josei", + "value": "genre/josei" + }, + { + "label": "Litrpg", + "value": "genre/litrpg" + }, + { + "label": "Magic", + "value": "genre/magic" + }, + { + "label": "Magical realism", + "value": "genre/magical-realism" + }, + { + "label": "Martial arts", + "value": "genre/martial-arts" + }, + { + "label": "Mature", + "value": "genre/mature" + }, + { + "label": "Mecha", + "value": "genre/mecha" + }, + { + "label": "Military", + "value": "genre/military" + }, + { + "label": "Modern life", + "value": "genre/modern-life" + }, + { + "label": "Myster", + "value": "genre/myster" + }, + { + "label": "Mystery", + "value": "genre/mystery" + }, + { + "label": "Other", + "value": "genre/other" + }, + { + "label": "Other", + "value": "genre/other" + }, + { + "label": "Psychological", + "value": "genre/psychological" + }, + { + "label": "Reincarnation", + "value": "genre/reincarnation" + }, + { + "label": "Romance", + "value": "genre/romance" + }, + { + "label": "Romance.smut", + "value": "genre/romance.smut" + }, + { + "label": "School life", + "value": "genre/school-life" + }, + { + "label": "Sci-fi", + "value": "genre/sci-fi" + }, + { + "label": "Seinen", + "value": "genre/seinen" + }, + { + "label": "Shoujo", + "value": "genre/shoujo" + }, + { + "label": "Shoujo ai", + "value": "genre/shoujo-ai" + }, + { + "label": "Shounen", + "value": "genre/shounen" + }, + { + "label": "Shounen ai", + "value": "genre/shounen-ai" + }, + { + "label": "Slice of life", + "value": "genre/slice-of-life" + }, + { + "label": "Smut", + "value": "genre/smut" + }, + { + "label": "Sports", + "value": "genre/sports" + }, + { + "label": "Supernatural", + "value": "genre/supernatural" + }, + { + "label": "System", + "value": "genre/system" + }, + { + "label": "Thriller", + "value": "genre/thriller" + }, + { + "label": "Tragedy", + "value": "genre/tragedy" + }, + { + "label": "Urban", + "value": "genre/urban" + }, + { + "label": "Urban life", + "value": "genre/urban-life" + }, + { + "label": "Video games", + "value": "genre/video-games" + }, + { + "label": "War", + "value": "genre/war" + }, + { + "label": "Wuxia", + "value": "genre/wuxia" + }, + { + "label": "Xianxia", + "value": "genre/xianxia" + }, + { + "label": "Xuanhuan", + "value": "genre/xuanhuan" + }, + { + "label": "Yaoi", + "value": "genre/yaoi" + }, + { + "label": "Yuri", + "value": "genre/yuri" + } + ] + }, + "complete": { + "type": "Switch", + "label": "Show Completed Novels Only", + "value": false + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/filters/novelfull.json b/plugins/multisrc/readnovelfull/filters/novelfull.json new file mode 100644 index 000000000..deaa9ce60 --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/novelfull.json @@ -0,0 +1,174 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "most-popular", + "options": [ + { + "label": "Hot Novel", + "value": "hot-novel" + }, + { + "label": "Completed Novel", + "value": "completed-novel" + }, + { + "label": "Most Popular", + "value": "most-popular" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { + "label": "Shounen", + "value": "genre/Shounen" + }, + { + "label": "Harem", + "value": "genre/Harem" + }, + { + "label": "Comedy", + "value": "genre/Comedy" + }, + { + "label": "Martial Arts", + "value": "genre/Martial+Arts" + }, + { + "label": "School Life", + "value": "genre/School+Life" + }, + { + "label": "Mystery", + "value": "genre/Mystery" + }, + { + "label": "Shoujo", + "value": "genre/Shoujo" + }, + { + "label": "Romance", + "value": "genre/Romance" + }, + { + "label": "Sci-fi", + "value": "genre/Sci-fi" + }, + { + "label": "Gender Bender", + "value": "genre/Gender+Bender" + }, + { + "label": "Mature", + "value": "genre/Mature" + }, + { + "label": "Fantasy", + "value": "genre/Fantasy" + }, + { + "label": "Horror", + "value": "genre/Horror" + }, + { + "label": "Drama", + "value": "genre/Drama" + }, + { + "label": "Tragedy", + "value": "genre/Tragedy" + }, + { + "label": "Supernatural", + "value": "genre/Supernatural" + }, + { + "label": "Ecchi", + "value": "genre/Ecchi" + }, + { + "label": "Xuanhuan", + "value": "genre/Xuanhuan" + }, + { + "label": "Adventure", + "value": "genre/Adventure" + }, + { + "label": "Action", + "value": "genre/Action" + }, + { + "label": "Psychological", + "value": "genre/Psychological" + }, + { + "label": "Xianxia", + "value": "genre/Xianxia" + }, + { + "label": "Wuxia", + "value": "genre/Wuxia" + }, + { + "label": "Historical", + "value": "genre/Historical" + }, + { + "label": "Slice of Life", + "value": "genre/Slice+of+Life" + }, + { + "label": "Seinen", + "value": "genre/Seinen" + }, + { + "label": "Lolicon", + "value": "genre/Lolicon" + }, + { + "label": "Adult", + "value": "genre/Adult" + }, + { + "label": "Josei", + "value": "genre/Josei" + }, + { + "label": "Sports", + "value": "genre/Sports" + }, + { + "label": "Smut", + "value": "genre/Smut" + }, + { + "label": "Mecha", + "value": "genre/Mecha" + }, + { + "label": "Yaoi", + "value": "genre/Yaoi" + }, + { + "label": "Shounen Ai", + "value": "genre/Shounen+Ai" + }, + { + "label": "History", + "value": "genre/History" + }, + { + "label": "Martial", + "value": "genre/Martial" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/filters/readnovelfull.json b/plugins/multisrc/readnovelfull/filters/readnovelfull.json new file mode 100644 index 000000000..f2687f34d --- /dev/null +++ b/plugins/multisrc/readnovelfull/filters/readnovelfull.json @@ -0,0 +1,174 @@ +{ + "filters": { + "type": { + "type": "Picker", + "label": "Novel Listing", + "value": "novel-list/most-popular-novel", + "options": [ + { + "label": "Hot Novel", + "value": "novel-list/hot-novel" + }, + { + "label": "Completed Novel", + "value": "novel-list/completed-novel" + }, + { + "label": "Most Popular", + "value": "novel-list/most-popular-novel" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre", + "value": "", + "options": [ + { + "label": "Action", + "value": "genres/action" + }, + { + "label": "Adult", + "value": "genres/adult" + }, + { + "label": "Adventure", + "value": "genres/adventure" + }, + { + "label": "Comedy", + "value": "genres/comedy" + }, + { + "label": "Drama", + "value": "genres/drama" + }, + { + "label": "Eastern", + "value": "genres/eastern" + }, + { + "label": "Ecchi", + "value": "genres/ecchi" + }, + { + "label": "Fantasy", + "value": "genres/fantasy" + }, + { + "label": "Game", + "value": "genres/game" + }, + { + "label": "Gender Bender", + "value": "genres/gender+bender" + }, + { + "label": "Harem", + "value": "genres/harem" + }, + { + "label": "Historical", + "value": "genres/historical" + }, + { + "label": "Horror", + "value": "genres/horror" + }, + { + "label": "Josei", + "value": "genres/josei" + }, + { + "label": "Lolicon", + "value": "genres/lolicon" + }, + { + "label": "Martial Arts", + "value": "genres/martial+arts" + }, + { + "label": "Mature", + "value": "genres/mature" + }, + { + "label": "Mecha", + "value": "genres/mecha" + }, + { + "label": "Modern Life", + "value": "genres/modern+life" + }, + { + "label": "Mystery", + "value": "genres/mystery" + }, + { + "label": "Psychological", + "value": "genres/psychological" + }, + { + "label": "Reincarnation", + "value": "genres/reincarnation" + }, + { + "label": "Romance", + "value": "genres/romance" + }, + { + "label": "School life", + "value": "genres/school+life" + }, + { + "label": "Sci-fi", + "value": "genres/sci-fi" + }, + { + "label": "Seinen", + "value": "genres/seinen" + }, + { + "label": "Shoujo", + "value": "genres/shoujo" + }, + { + "label": "Shounen", + "value": "genres/shounen" + }, + { + "label": "Slice of Life", + "value": "genres/slice+of+life" + }, + { + "label": "Smut", + "value": "genres/smut" + }, + { + "label": "Sports", + "value": "genres/sports" + }, + { + "label": "Supernatural", + "value": "genres/supernatural" + }, + { + "label": "System", + "value": "genres/system" + }, + { + "label": "Thriller", + "value": "genres/thriller" + }, + { + "label": "Tragedy", + "value": "genres/tragedy" + }, + { + "label": "Transmigration", + "value": "genres/transmigration" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readnovelfull/generator.js b/plugins/multisrc/readnovelfull/generator.js new file mode 100644 index 000000000..b897ed2d6 --- /dev/null +++ b/plugins/multisrc/readnovelfull/generator.js @@ -0,0 +1,39 @@ +import list from './sources.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(source => { + const exist = existsSync(join(folder, 'filters', source.id + '.json')); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.id + '.json'), + ); + source.filters = JSON.parse(filters).filters; + } + console.log( + `[readnovelfull] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const readNovelFullTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${readNovelFullTemplate.replace('// CustomJS HERE', source.options?.customJs || '')} +const plugin = new ReadNovelFullPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + return { + lang: source.options?.lang || 'English', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/readnovelfull/get_filters.js b/plugins/multisrc/readnovelfull/get_filters.js new file mode 100644 index 000000000..69bcbd8ac --- /dev/null +++ b/plugins/multisrc/readnovelfull/get_filters.js @@ -0,0 +1,225 @@ +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import * as readline from 'readline'; +import process from 'process'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +function extractValueFromHref(href, baseUrl, name, isGenre = false) { + const fullUrl = new URL(decodeURI(href), baseUrl); + const params = fullUrl.searchParams; + + if (isGenre && params.get('type') === 'category_novel' && params.has('id')) { + return params.get('id'); + } + + const value = params.has('type') + ? params.get('type') + : fullUrl.pathname.substring(1) || ''; + + if (!value) { + console.warn( + `Skipping option for ${name}: Could not determine value from href "${href}"`, + ); + } + return value; +} + +function getFilters(name, html, baseUrl) { + const $ = cheerio.load(html); + const filters = { + filters: { + 'type': { + type: 'Picker', + label: 'Novel Listing', + value: '', + options: [], + }, + 'genres': { + type: 'Picker', + label: 'Genre', + value: '', + options: [], + }, + }, + }; + + const baseSelector = 'div.navbar-collapse li.dropdown'; + + // --- Type Filters --- + let defaultValue = null; + const typeOptions = []; + $(`${baseSelector}:eq(0) li a`).each((_, el) => { + const $el = $(el); + const title = $el.attr('title'); + const label = $el.text().trim(); + const href = $el.attr('href'); + + if (!href || !label || title === 'Latest Release') return; + + const value = extractValueFromHref(href, baseUrl, name); + + if (value) { + typeOptions.push({ label, value }); + if (title === 'Most Popular') { + defaultValue = value; + } + } + }); + filters.filters.type.options = typeOptions; + filters.filters.type.value = defaultValue ?? (typeOptions[0]?.value || ''); // Set default: Popular > First > Empty + + // --- Genres Filters --- + const genreOptions = []; + $(`${baseSelector}:eq(1) li a`).each((_, el) => { + const $el = $(el); + const label = $el.text().trim(); + const href = $el.attr('href'); + + if (!href || !label) return; + + const value = extractValueFromHref(href, baseUrl, name, true); + + if (value) { + genreOptions.push({ label, value }); + } + }); + filters.filters.genres.options = genreOptions; // Genre has no default + + // --- Validation and Saving --- + if ( + filters.filters.type.options.length === 0 || + filters.filters.genres.options.length === 0 + ) { + console.warn( + `🚨Warning for ${name}: Type or Genre options might be incomplete or empty. (${path.join(__dirname, 'filters', name + '.json')})🚨`, + ); + } + try { + const filtersDir = path.join(__dirname, 'filters'); + if (!fs.existsSync(filtersDir)) + fs.mkdirSync(filtersDir, { recursive: true }); + fs.writeFileSync( + path.join(filtersDir, name + '.json'), + JSON.stringify(filters, null, 2), + ); + console.log(`✅Filters created successfully for ${name}✅`); + } catch (error) { + console.error( + `🚨Error writing filters file for ${name}: ${error.message}🚨`, + ); + } +} + +async function getFiltersFromURL(name, url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `HTTP error! status: ${response.status}, while fetching ${response.url}`, + ); + } + const html = await response.text(); + try { + getFilters(name, html, url); + } catch (e) { + console.error('Error while getting filters from', url, e); + } +} + +const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, +}); + +async function askGetFilter() { + try { + const sources = JSON.parse( + fs.readFileSync(path.join(__dirname, 'sources.json'), 'utf-8'), + ); + + rl.question( + 'Enter the id of the site (same one as in sources.json): ', + async name => { + const baseUrl = await getBaseUrl(name, sources); + + rl.question( + EREASE_PREV_LINE + + "Do you want to get the filters from a URL or the html text? (if url doesn't work try html) (url/html): ", + async method => { + if (method.toLowerCase() === 'url') { + if (baseUrl) { + console.log('Getting filters from', baseUrl); + try { + await getFiltersFromURL(name, baseUrl); + } catch (e) { + console.error('Error while getting filters from', baseUrl); + console.log(e.message || e); + } + } else { + console.error( + 'Cannot get filters from URL: Base URL is not available.', + ); + } + rl.close(); + } else { + rl.question( + EREASE_PREV_LINE + 'Enter the absolute path to the HTML file: ', + async filePath => { + try { + const html = fs.readFileSync(filePath, 'utf-8'); + if (baseUrl) { + console.log('Using base URL:', baseUrl); + } else { + console.error( + 'Cannot get filters from HTML: Base URL is not available.', + ); + } + getFilters(name, html, baseUrl); + } catch (e) { + console.error( + 'Error reading HTML file or getting filters:', + e.message || e, + ); + } finally { + rl.close(); + } + }, + ); + } + }, + ); + }, + ); + } catch (e) { + console.error('Error reading sources.json:', e.message || e); + rl.close(); + } +} + +askGetFilter(); + +async function getBaseUrl(name, sources) { + const source = sources.find(s => s.id === name); + if (source && source.sourceSite) { + console.log('Using base URL from sources.json:', source.sourceSite); + return source.sourceSite; + } else { + console.warn( + `Source with id "${name}" not found or missing sourceSite in sources.json.`, + ); + return new Promise(resolve => { + rl.question( + EREASE_PREV_LINE + 'Enter the base URL for the site: ', + manualBaseUrl => { + resolve(manualBaseUrl); + }, + ); + }); + } +} + +export { getFiltersFromURL }; diff --git a/plugins/multisrc/readnovelfull/sources.json b/plugins/multisrc/readnovelfull/sources.json new file mode 100644 index 000000000..614512996 --- /dev/null +++ b/plugins/multisrc/readnovelfull/sources.json @@ -0,0 +1,94 @@ +[ + { + "id": "readnovelfull", + "sourceSite": "https://readnovelfull.com/", + "sourceName": "ReadNovelFull", + "options": { + "latestPage": "novel-list/latest-release-novel", + "searchPage": "novel-list/search" + } + }, + { + "id": "allnovel", + "sourceSite": "https://allnovel.org/", + "sourceName": "AllNovel", + "options": { + "latestPage": "latest-release-novel", + "searchPage": "search", + "chapterListing": "ajax-chapter-option" + } + }, + { + "id": "lightnovelplus", + "sourceSite": "https://lightnovelplus.com/", + "sourceName": "LightNovelPlus", + "options": { + "novelListing": "book/bookclass.html", + "searchPage": "book/search.html", + "chapterListing": "get_chapter_list", + "chapterParam": "bookId", + "latestPage": "last_release", + "pageParam": "page_num", + "langParam": "language", + "urlLangCode": "en" + } + }, + { + "id": "novelfull", + "sourceSite": "https://novelfull.com/", + "sourceName": "NovelFull", + "options": { + "latestPage": "latest-release-novel", + "searchPage": "search", + "chapterListing": "ajax-chapter-option" + } + }, + { + "id": "libread", + "sourceSite": "https://libread.com/", + "sourceName": "Lib Read", + "options": { + "latestPage": "sort/latest-novels", + "searchPage": "search", + "searchKey": "searchkey", + "postSearch": true, + "noAjax": true, + "noPages": ["sort/most-popular"], + "pageAsPath": true + } + }, + { + "id": "anf.net", + "sourceSite": "https://novgo.net/", + "sourceName": "AllNovelFull", + "options": { + "latestPage": "latest-release-novel", + "searchPage": "search", + "chapterListing": "ajax-chapter-option" + } + }, + { + "id": "novelbin", + "sourceSite": "https://novelbin.com/", + "sourceName": "Novel Bin", + "options": { + "latestPage": "sort/latest", + "searchPage": "search" + } + }, + { + "id": "FWN.com", + "sourceSite": "https://freewebnovel.com/", + "sourceName": "Free Web Novel", + "options": { + "latestPage": "sort/latest-novels", + "searchPage": "search", + "searchKey": "searchkey", + "postSearch": true, + "noAjax": true, + "noPages": ["sort/most-popular"], + "pageAsPath": true, + "customJs": "$('.txt, #chr-content, #chapter-content').find('*').addBack().contents().filter((_, el) => el.type === 'text').each((_, el) => { el.data = el.data.replace(/(?:𝐟|ᵮ|𝑓|𝒇|𝒻|𝓯|𝔣|𝕗|𝖿|𝗳|𝙛|𝚏|ꬵ|ꞙ|ẝ|𝖋|ⓕ|f|ƒ|ḟ|ʃ|բ|ᶠ|⒡|ſ|ꊰ|ʄ|∱|ᶂ|𝘧|\\bf)(?:𝚛|ꭇ|ᣴ|ℾ|𝚪|𝛤|𝜞|𝝘|𝞒|Ⲅ|Г|Ꮁ|ᒥ|ꭈ|ⲅ|ꮁ|ⓡ|r|ŕ|ṙ|ř|ȑ|ȓ|ṛ|ṝ|ŗ|г|Ր|ɾ|ᥬ|ṟ|ɍ|ʳ|⒭|ɼ|ѓ|ᴦ|ᶉ|𝐫|𝑟|𝒓|𝓇|𝓻|𝔯|𝕣|𝖗|𝗋|𝗿|𝘳|𝙧|ᵲ|ґ|ᵣ|r)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)+(?:𝐰|ꝡ|𝑤|𝒘|𝓌|𝔀|𝔴|𝕨|𝖜|𝗐|𝘄|𝘸|𝙬|𝚠|ա|ẁ|ꮃ|ẃ|ⓦ|⍵|ŵ|ẇ|ẅ|ẘ|ẉ|ⱳ|ὼ|ὠ|ὡ|ὢ|ὣ|ω|ὤ|ὥ|ὦ|ὧ|ῲ|ῳ|ῴ|ῶ|ῷ|Ⱳ|ѡ|ԝ|ᴡ|ώ|ᾠ|ᾡ|ᾢ|ᾣ|ᾤ|ᾥ|ᾦ|ɯ|𝝕|𝟉|𝞏|w)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ꮟ|Ꮟ|𝐛|𝘣|𝒷|𝔟|𝓫|𝖇|𝖻|𝑏|𝙗|𝕓|𝒃|𝗯|𝚋|♭|ᑳ|ᒈ|b|ᖚ|ᕹ|ᕺ|ⓑ|ḃ|ḅ|ҍ|ъ|ḇ|ƃ|ɓ|ƅ|ᖯ|Ƅ|Ь|ᑲ|þ|Ƃ|⒝|Ъ|ᶀ|ᑿ|ᒀ|ᒂ|ᒁ|ᑾ|ь|ƀ|Ҍ|Ѣ|ѣ|ᔎ |b)(?:ո|ռ|ח|𝒏|𝓷|𝙣|𝑛|𝖓|𝔫|𝗇|𝚗|𝗻|ᥒ|ⓝ|ή|n|ǹ|ᴒ|ń|ñ|ᾗ|η|ṅ|ň|ṇ|ɲ|ņ|ṋ|ṉ|ղ|ຖ|Ռ|ƞ|ŋ|⒩|ภ|ก|ɳ|п|ʼn|л|ԉ|Ƞ|ἠ|ἡ|ῃ|դ|ᾐ|ᾑ|ᾒ|ᾓ|ᾔ|ᾕ|ᾖ|ῄ|ῆ|ῇ|ῂ|ἢ|ἣ|ἤ|ἥ|ἦ|ἧ|ὴ|ή|በ|ቡ|ቢ|ባ|ቤ|ብ|ቦ|ȵ|𝛈|𝜂|𝜼|𝝶|𝞰|𝕟|𝘯|𝐧|𝓃|ᶇ|ᵰ|ᥥ|∩|n)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:∨|⌄|⋁|ⅴ|𝐯|𝑣|𝒗|𝓋|𝔳|𝕧|𝖛|𝗏|ꮩ|ሀ|ⓥ|v|𝜐|𝝊|ṽ|ṿ|౮|ง|ѵ|ע|ᴠ|ν|ט|ᵥ|ѷ|៴|ᘁ|𝙫|𝚟|𝛎|𝜈|𝝂|𝝼|𝞶|𝘷|𝘃|𝓿|v)(?:ə|ә|ⅇ|ꬲ|ꞓ|⋴|𝛆|𝛜|𝜀|𝜖|𝜺|𝝐|𝝴|𝞊|𝞮|𝟄|ⲉ|ꮛ|𐐩|Ꞓ|Ⲉ|⍷|𝑒|𝓮|𝕖|𝖊|𝘦|𝗲|𝚎|𝙚|𝒆|𝔢|𝖾|𝐞|Ҿ|ҿ|ⓔ|e|⒠|è|ᧉ|é|ᶒ|ê|ɘ|ἔ|ề|ế|ễ|૯|ǝ|є|ε|ē|ҽ|ɛ|ể|ẽ|ḕ|ḗ|ĕ|ė|ë|ẻ|ě|ȅ|ȇ|ẹ|ệ|ȩ|ɇ|ₑ|ę|ḝ|ḙ|ḛ|℮|е|ԑ|ѐ|ӗ|ᥱ|ё|ἐ|ἑ|ἒ|ἓ|ἕ|ℯ|e)(?:ⓛ|l|ŀ|ĺ|ľ|ḷ|ḹ|ļ|Ӏ|ℓ|ḽ|ḻ|ł|レ|ɭ|ƚ|ɫ|ⱡ|\\||Ɩ|⒧|ʅ|ǀ|ו|ן|Ι|І|||ᶩ|ӏ|𝓘|𝕀|𝖨|𝗜|𝘐|𝐥|𝑙|𝒍|𝓁|𝔩|𝕝|𝖑|𝗅|𝗹|𝘭|𝚕|𝜤|𝝞|ı|𝚤|ɩ|ι|𝛊|𝜄|𝜾|𝞲|I|l)(?:.?(?:🝌|c|ⅽ|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|ᴄ|ϲ|ⲥ|с|ꮯ|𐐽|ⲥ|𐐽|ꮯ|ĉ|c|ⓒ|ć|č|ċ|ç|ҁ|ƈ|ḉ|ȼ|ↄ|с|ር|ᴄ|ϲ|ҫ|꒝|ς|ɽ|ϛ|𝙲|ᑦ|᧚|𝐜|𝑐|𝒄|𝒸|𝓬|𝔠|𝕔|𝖈|𝖼|𝗰|𝘤|𝙘|𝚌|₵|🇨|ᥴ|ᒼ|ⅽ|c)(?:ం|ಂ|ം|ං|૦|௦|۵|ℴ|𝑜|𝒐|𝖔|ꬽ|𝝄|𝛔|𝜎|𝝈|𝞂|ჿ|𝚘|০|୦|ዐ|𝛐|𝗈|𝞼|ဝ|ⲟ|𝙤|၀|𐐬|𝔬|𐓪|𝓸|🇴|⍤|○|ϙ|🅾|𝒪|𝖮|𝟢|𝟶|𝙾|𝘰|𝗼|𝕠|𝜊|𝐨|𝝾|𝞸|ᐤ|ⓞ|ѳ|᧐|ᥲ|ð|o|ఠ|ᦞ|Փ|ò|ө|ӧ|ó|º|ō|ô|ǒ|ȏ|ŏ|ồ|ȭ|ṏ|ὄ|ṑ|ṓ|ȯ|ȫ|๏|ᴏ|ő|ö|ѻ|о|ዐ|ǭ|ȱ|০|୦|٥|౦|೦|൦|๐|໐|ο|օ|ᴑ|०|੦|ỏ|ơ|ờ|ớ|ỡ|ở|ợ|ọ|ộ|ǫ|ø|ǿ|ɵ|ծ|ὀ|ὁ|ό|ὸ|ό|ὂ|ὃ|ὅ|o)(?:₥|ᵯ|𝖒|𝐦|𝗆|𝔪|𝕞|𝓂|ⓜ|m|ന|ᙢ|൩|ḿ|ṁ|ⅿ|ϻ|ṃ|ጠ|ɱ|៳|ᶆ|𝒎|𝙢|𝓶|𝚖|𝑚|𝗺|᧕|᧗|m))?/g, ''); });" + } + } +] diff --git a/plugins/multisrc/readnovelfull/template.ts b/plugins/multisrc/readnovelfull/template.ts new file mode 100644 index 000000000..5afd9eb44 --- /dev/null +++ b/plugins/multisrc/readnovelfull/template.ts @@ -0,0 +1,777 @@ +import { Parser } from 'htmlparser2'; +import { fetchApi, FetchInit } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters } from '@libs/filterInputs'; +import { load } from 'cheerio'; + +type ReadNovelFullOptions = { + lang?: string; + versionIncrements?: number; + latestPage: string; + searchPage: string; + chapterListing?: string; + chapterParam?: string; + pageParam?: string; + novelListing?: string; + typeParam?: string; + genreParam?: string; + genreKey?: string; + langParam?: string; + urlLangCode?: string; + searchKey?: string; + postSearch?: boolean; + noAjax?: boolean; + noPages?: string[]; + pageAsPath?: boolean; + customJs?: string; +}; + +export type ReadNovelFullMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options: ReadNovelFullOptions; + filters?: Filters; +}; + +export class ReadNovelFullPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + options: ReadNovelFullOptions; + filters?: Filters | undefined; + + constructor(metadata: ReadNovelFullMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/readnovelfull/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `2.2.${1 + versionIncrements}`; + this.options = metadata.options; + this.filters = metadata.filters; + } + + lastSearch: number | null = null; + searchInterval = 3400; + + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + parseNovels(html: string) { + const novels: Plugin.NovelItem[] = []; + let tempNovel: Partial<Plugin.NovelItem> = {}; + let depth: number; + + const stateStack: ParsingState[] = [ParsingState.Idle]; + const currentState = () => stateStack[stateStack.length - 1]; + const pushState = (state: ParsingState) => stateStack.push(state); + const popState = () => + stateStack.length > 1 ? stateStack.pop() : currentState(); + + const parser = new Parser({ + onopentag: (name, attribs) => { + const state = currentState(); + if ( + attribs.class?.includes('archive') || + attribs.class === 'col-content' + ) { + pushState(ParsingState.NovelList); + depth = 0; + } + + if ( + state !== ParsingState.NovelList && + state !== ParsingState.NovelName + ) + return; + + switch (name) { + case 'img': + { + const cover = attribs['data-src'] || attribs.src; + if (cover) { + tempNovel.cover = new URL(cover, this.site).href; + } + } + break; + case 'h3': + if (state === ParsingState.NovelList) { + pushState(ParsingState.NovelName); + } + break; + case 'a': + if (state === ParsingState.NovelName) { + const href = attribs.href; + if (href) { + tempNovel.path = new URL(href, this.site).pathname.substring(1); + tempNovel.name = attribs.title; + } + } + break; + case 'div': + depth++; + break; + default: + return; + } + }, + + onclosetag: name => { + const state = currentState(); + if (name === 'a' && state === ParsingState.NovelName) { + if (tempNovel.name && tempNovel.path) { + novels.push({ ...tempNovel } as Plugin.NovelItem); + } + tempNovel = {}; + popState(); + } + if (name === 'div' && state === ParsingState.NovelList) { + depth--; + if (depth < 0) popState(); + } + }, + }); + + parser.write(html); + parser.end(); + + return novels; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const { + pageParam = 'page', + novelListing, + typeParam = 'type', + latestPage, + genreParam = 'category_novel', + genreKey = 'id', + langParam, + urlLangCode, + noPages = [], + pageAsPath = false, + } = this.options; + + // Skip Pagination for FWN & LR + if ( + pageNo !== 1 && + !showLatestNovels && + !filters.genres.value.length && + noPages.length > 0 && + noPages.includes(filters.type.value) + ) { + return []; + } + + let url = ''; + + if (novelListing) { + // URL structure with parameters + const params = new URLSearchParams(); + + if (showLatestNovels) { + params.append(typeParam, latestPage); + } else if (filters.genres.value.length) { + params.append(typeParam, genreParam); + params.append(genreKey, filters.genres.value); + } else { + params.append(typeParam, filters.type.value); + } + + // Add language parameter if specified + if (langParam && urlLangCode) { + params.append(langParam, urlLangCode); + } + + params.append(pageParam, pageNo.toString()); + url = `${this.site}${novelListing}?${params.toString()}`; + } else { + // URL structure with path segments + const basePage = showLatestNovels + ? latestPage + : filters.genres.value.length + ? filters.genres.value + : filters.type.value; + + if (pageAsPath) { + if (pageNo > 1) { + url = `${this.site}${basePage}/${pageNo.toString()}`; + } else { + url = `${this.site}${basePage}`; + } + } else { + url = `${this.site}${basePage}?${pageParam}=${pageNo.toString()}`; + } + } + + const result = await fetchApi(url); + if (!result.ok) { + throw new Error( + `Could not reach site (${result.status}: ${result.statusText}) try to open in webview.`, + ); + } + const html = await result.text(); + return this.parseNovels(html); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + const novel: Partial<Plugin.SourceNovel> = { + path: novelPath, + chapters: [], + }; + const summaryParts: string[] = []; + const statusParts: string[] = []; + const authorParts: string[] = []; + const genreArray: string[] = []; + const infoParts: string[] = []; + const chapters: Plugin.ChapterItem[] = []; + let novelId: string | null = null; + let tempChapter: Partial<Plugin.ChapterItem> = {}; + let i = 0; + let depth: number; + + const stateStack: ParsingState[] = [ParsingState.Idle]; + const currentState = () => stateStack[stateStack.length - 1]; + const pushState = (state: ParsingState) => stateStack.push(state); + const popState = () => + stateStack.length > 1 ? stateStack.pop() : currentState(); + + const parser = new Parser({ + onopentag: (name, attribs) => { + const state = currentState(); + switch (name) { + case 'div': + switch (attribs.class) { + case 'books': + case 'm-imgtxt': + pushState(ParsingState.Cover); + return; + case 'inner': + case 'desc-text': + if (state === ParsingState.Cover) popState(); + pushState(ParsingState.Summary); + break; + case 'info': + pushState(ParsingState.Info); + depth = 0; + break; + } + if (!this.options.noAjax && attribs.id === 'rating') { + novelId = attribs['data-novel-id']; + } + if (state === ParsingState.Info) depth++; + break; + case 'img': + if (state === ParsingState.Cover) { + const cover = + attribs.src ?? attribs['data-cfsrc'] ?? attribs['data-src']; + const name = attribs.title; + if (cover) { + novel.cover = new URL(cover, this.site).href; + } + if (name) { + novel.name = name; + } else { + popState(); + } + } + break; + case 'h3': + if (state === ParsingState.Cover) { + pushState(ParsingState.NovelName); + } + break; + case 'span': + if (state === ParsingState.Cover && attribs.title) { + const newState = { + 'Genre': ParsingState.Genres, + 'Author': ParsingState.Author, + 'Status': ParsingState.Status, + }[attribs.title]; + + if (newState) pushState(newState); + } + break; + case 'br': + if (state === ParsingState.Summary) { + summaryParts.push('\n'); + } + break; + case 'ul': + if (attribs.class?.includes('info-meta')) { + pushState(ParsingState.Info); + } + if (this.options.noAjax && attribs.id === 'idData') { + pushState(ParsingState.ChapterList); + } + break; + case 'a': + if (state === ParsingState.ChapterList) { + i++; + const href = attribs.href; + pushState(ParsingState.Chapter); + + tempChapter.name = attribs.title || `Chapter ${i}`; + tempChapter.releaseTime = null; + tempChapter.chapterNumber = i; + tempChapter.path = + href?.substring(1) || + novelPath.replace('.html', `/chapter-${i}.html`); + } + break; + } + }, + + ontext: data => { + const text = data.trim(); + if (!text) return; + + switch (currentState()) { + case ParsingState.NovelName: + novel.name = (novel.name || '') + text; + break; + case ParsingState.Summary: + summaryParts.push(data); + break; + case ParsingState.Info: + infoParts.push(text); + break; + case ParsingState.Genres: + genreArray.push(data); + break; + case ParsingState.Author: + authorParts.push(data); + break; + case ParsingState.Status: + statusParts.push(text); + break; + } + }, + + onclosetag: name => { + const state = currentState(); + switch (name) { + case 'div': + switch (state) { + case ParsingState.Info: + depth--; + infoParts.push('\n'); + if (depth < 0) { + popState(); + } + break; + case ParsingState.Genres: + case ParsingState.Author: + case ParsingState.Status: + case ParsingState.Summary: + popState(); + break; + } + break; + case 'h3': + if (state === ParsingState.NovelName) { + popState(); + } + break; + case 'a': + if (state === ParsingState.Chapter) { + if (tempChapter.name && tempChapter.path) { + chapters.push({ ...tempChapter } as Plugin.ChapterItem); + } + tempChapter = {}; + popState(); + } + break; + case 'li': + if (state === ParsingState.Info) { + infoParts.push('\n'); + } + break; + case 'ul': + switch (state) { + case ParsingState.Info: + case ParsingState.ChapterList: + popState(); + break; + } + break; + default: + return; + } + }, + + onend: () => { + if (infoParts.length) { + infoParts + .join('') + .split('\n') + .map(line => line.trim()) + .filter(line => line.includes(':')) + .forEach(line => { + const parts = line.split(':'); + const detailName = parts[0].trim().toLowerCase(); + const detail = parts[1] + .split(',') + .map(g => g.trim()) + .join(', '); + + switch (detailName) { + case 'author': + novel.author = detail; + break; + case 'genre': + novel.genres = detail; + break; + case 'status': + { + const map: Record<string, string> = { + ongoing: NovelStatus.Ongoing, + hiatus: NovelStatus.OnHiatus, + dropped: NovelStatus.Cancelled, + cancelled: NovelStatus.Cancelled, + completed: NovelStatus.Completed, + }; + novel.status = + map[detail.toLowerCase()] ?? NovelStatus.Unknown; + } + break; + default: + return; + } + }); + + if (!novelId) { + const idMatch = novelPath.match(/\d+/); + novelId = idMatch ? idMatch[0] : null; + } + } else { + novel.genres = genreArray.join('').trim(); + novel.author = authorParts.join('').trim(); + novel.status = statusParts + .join('') + .toLowerCase() + .replace(/\b\w/g, char => char.toUpperCase()); + } + novel.summary = summaryParts.join('\n\n').trim(); + }, + }); + + parser.write(body); + parser.end(); + + if (this.options.noAjax && chapters.length > 0) { + novel.chapters = chapters; + } else if (novelId !== null) { + const chapterListing = + this.options.chapterListing || 'ajax/chapter-archive'; + const ajaxParam = this.options.chapterParam || 'novelId'; + const params = new URLSearchParams({ [ajaxParam]: novelId }); + const chaptersUrl = `${this.site}${chapterListing}?${params.toString()}`; + + const ajaxResult = await fetchApi(chaptersUrl); + if (!ajaxResult.ok) { + console.error(`Failed to fetch chapters: ${ajaxResult.status}`); + novel.chapters = []; + } else { + const ajaxBody = await ajaxResult.text(); + const ajaxChapters: Plugin.ChapterItem[] = []; + let tempAjaxChapter: Partial<Plugin.ChapterItem> = {}; + + const ajaxParser = new Parser({ + onopentag: (name, attribs) => { + let chapterHref: string | undefined; + let initialName: string | undefined; + + if (name === 'a' && attribs.href) { + chapterHref = attribs.href; + initialName = attribs.title || ''; + pushState(ParsingState.Chapter); + } else if (name === 'option' && attribs.value) { + chapterHref = attribs.value; + initialName = ''; + pushState(ParsingState.Chapter); + } + + if (chapterHref !== undefined) { + const href = new URL(chapterHref, this.site); + tempAjaxChapter.path = href.pathname.substring(1); + tempAjaxChapter.name = initialName; + } + }, + + ontext: data => { + const text = data.trim(); + if ( + currentState() === ParsingState.Chapter && + !tempAjaxChapter.name && + text + ) { + tempAjaxChapter.name += text; + } + }, + + onclosetag: name => { + if ( + (name === 'a' || name === 'option') && + currentState() === ParsingState.Chapter + ) { + if (tempAjaxChapter.name && tempAjaxChapter.path) { + tempAjaxChapter.name = tempAjaxChapter.name.trim(); + tempAjaxChapter.releaseTime = null; + ajaxChapters.push({ + ...tempAjaxChapter, + } as Plugin.ChapterItem); + } + tempAjaxChapter = {}; + popState(); + } + }, + }); + + ajaxParser.write(ajaxBody); + ajaxParser.end(); + novel.chapters = ajaxChapters; + } + } + + return novel as Plugin.SourceNovel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const response = await fetchApi(this.site + chapterPath); + let html = await response.text(); + if (this.options?.customJs) { + try { + const $ = load(html); + // CustomJS HERE + html = $.html(); + } catch (error) { + console.error('Error executing customJs:', error); + throw error; + } + } + + let depth: number; + let depthHide: number; + const chapterHtml: string[] = []; + let skipClosingTag = false; + let currentTagToSkip = ''; + + const stateStack: ParsingState[] = [ParsingState.Idle]; + const currentState = () => stateStack[stateStack.length - 1]; + const pushState = (state: ParsingState) => stateStack.push(state); + const popState = () => + stateStack.length > 1 ? stateStack.pop() : currentState(); + + const escapeMap: Record<string, string> = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + ' ': ' ', + '\u200C': '', // this is probably a breaking change, report if paragraphs look weird + }; + const escapeHtml = (text: string) => + text.replace(/[&<>"'\xA0\u200C]/g, char => escapeMap[char]); + + const parser = new Parser({ + onopentag(name, attribs) { + const state = currentState(); + const attrib = attribs.class?.trim(); + switch (state) { + case ParsingState.Idle: + if ( + attrib === 'txt' || + attribs.id === 'chr-content' || + attribs.id === 'chapter-content' + ) { + pushState(ParsingState.Chapter); + depth = 0; + } + break; + case ParsingState.Chapter: + if (name === 'sub' || name === 'iframe') { + pushState(ParsingState.Hidden); + } else if (name === 'div') { + depth++; + if ( + attrib?.includes('unlock-buttons') || + attrib?.includes('ads') + ) { + pushState(ParsingState.Hidden); + depthHide = 0; + } + } + break; + case ParsingState.Hidden: + if (name === 'sub') { + // Allow nesting of hidden states if a sub is inside a div + pushState(ParsingState.Hidden); + } else if (name === 'div') { + depthHide++; + } + break; + default: + return; + } + + if (currentState() === ParsingState.Chapter) { + const attrKeys = Object.keys(attribs); + + if (attrKeys.length === 0) { + chapterHtml.push(`<${name}>`); + } else if (attrKeys.every(key => attribs[key].trim() === '')) { + // Handle tags with empty attributes as text content + // eg: novel/rising-up-from-a-nobleman-to-intergalactic-warlord/chapter-184 + skipClosingTag = true; + currentTagToSkip = name; + const uppercaseName = name.replace(/\b\w/g, char => + char.toUpperCase(), + ); + chapterHtml.push( + escapeHtml(`<${uppercaseName} ${attrKeys.join(' ')}>`), + ); + } else { + // Normal tag with attributes + const attrString = attrKeys + .map(key => ` ${key}="${attribs[key].replace(/"/g, '"')}"`) + .join(''); + chapterHtml.push(`<${name}${attrString}>`); + } + } + }, + + ontext(text) { + if (currentState() === ParsingState.Chapter) { + const data = escapeHtml(text); + chapterHtml.push(data.trim().replace(/\s\s+/, ' ')); + } + }, + + onclosetag(name) { + const state = currentState(); + + if (state === ParsingState.Hidden) { + if (name === 'sub' || name === 'iframe') { + popState(); + } else if (name === 'div') { + depthHide--; + if (depthHide < 0) { + popState(); + depth--; + } + } + } + + if (state !== ParsingState.Chapter) { + return; + } + + if (!parser['isVoidElement'](name)) { + if (skipClosingTag && name === currentTagToSkip) { + skipClosingTag = false; + currentTagToSkip = ''; + } else { + chapterHtml.push(`</${name}>`); + } + } + + if (name === 'div') { + depth--; + if (depth < 0) { + pushState(ParsingState.Stopped); + } + } + }, + }); + + parser.write(html); + parser.end(); + + return chapterHtml.join(''); + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const now = Date.now(); + if (this.lastSearch && now - this.lastSearch <= this.searchInterval) { + await this.sleep(this.searchInterval); + } + + const { + pageParam = 'page', + searchKey = 'keyword', + postSearch, + langParam, + urlLangCode, + searchPage, + } = this.options; + + const params = new URLSearchParams({ + [searchKey]: searchTerm, + ...(langParam && urlLangCode && { [langParam]: urlLangCode }), + ...(!postSearch && { [pageParam]: page.toString() }), + }); + + const url = `${this.site}${searchPage}${!postSearch ? `?${params.toString()}` : ''}`; + + const fetchOptions: FetchInit | undefined = postSearch + ? { + method: 'POST', + body: params.toString(), + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + } + : undefined; + + const result = await fetchApi(url, fetchOptions); + this.lastSearch = Date.now(); + + if (!result.ok) { + throw new Error( + `Could not reach site ('${result.status}') try to open in webview.`, + ); + } + + const html = await result.text(); + + // Check for alert error messages, ported over from cheerio TODO: confirm behaviour + const alertText = html.match(/alert\((.*?)\)/)?.[1] || ''; + if (alertText) throw new Error(alertText); + + return this.parseNovels(html); + } +} + +enum ParsingState { + Idle, + Info, + Cover, + Author, + Genres, + Status, + Hidden, + Summary, + Stopped, + Chapter, + ChapterList, + NovelName, + NovelList, +} diff --git a/plugins/multisrc/readwn/filters/fannovel.json b/plugins/multisrc/readwn/filters/fannovel.json new file mode 100644 index 000000000..98ab730c0 --- /dev/null +++ b/plugins/multisrc/readwn/filters/fannovel.json @@ -0,0 +1,7344 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Sort By", + "value": "onclick", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "all", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre / Category", + "value": "", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Anime", + "value": "anime" + }, + { + "label": "BG", + "value": "bg" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "BL", + "value": "bl" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Competitive Sports", + "value": "competitive-sports" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Crossing", + "value": "crossing" + }, + { + "label": "Derivative Fanfic", + "value": "derivative-fanfic" + }, + { + "label": "Detective", + "value": "detective" + }, + { + "label": "Douluo", + "value": "douluo" + }, + { + "label": "Dragon Ball", + "value": "dragon-ball" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Elf", + "value": "elf" + }, + { + "label": "Faloo", + "value": "faloo" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Football", + "value": "football" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "GL", + "value": "gl" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Historical Romance", + "value": "historical-romance" + }, + { + "label": "Hogwarts", + "value": "hogwarts" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Hot", + "value": "hot" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "LGBT", + "value": "lgbt" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Marvel", + "value": "marvel" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Modern Life", + "value": "modern-life" + }, + { + "label": "Movies", + "value": "movies" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Naruto", + "value": "naruto" + }, + { + "label": "NBA", + "value": "nba" + }, + { + "label": "One Piece", + "value": "one-piece" + }, + { + "label": "Other", + "value": "other" + }, + { + "label": "Pokemon", + "value": "pokemon" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Realistic Fiction", + "value": "realistic-fiction" + }, + { + "label": "Rebirth", + "value": "rebirth" + }, + { + "label": "Reincarnation", + "value": "reincarnation" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Science fiction", + "value": "science-fiction" + }, + { + "label": "Secret", + "value": "secret" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Sign in", + "value": "sign-in" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Suspense", + "value": "suspense" + }, + { + "label": "System", + "value": "system" + }, + { + "label": "Systemflow", + "value": "systemflow" + }, + { + "label": "Terror", + "value": "terror" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Travel Through Time", + "value": "travel-through-time" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Villain", + "value": "villain" + }, + { + "label": "War", + "value": "war" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "tags": { + "type": "Picker", + "label": "Tags", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Ability", + "value": "2734" + }, + { + "label": "Action", + "value": "10" + }, + { + "label": "Adventure", + "value": "27" + }, + { + "label": "Academy", + "value": "58" + }, + { + "label": "Anti-routi", + "value": "2807" + }, + { + "label": "Apocalypse", + "value": "187" + }, + { + "label": "Alchemy", + "value": "105" + }, + { + "label": "ArmyBuildi", + "value": "3638" + }, + { + "label": "ArrogantCh", + "value": "3675" + }, + { + "label": "AncientChi", + "value": "3648" + }, + { + "label": "Artifacts", + "value": "162" + }, + { + "label": "Alien", + "value": "2943" + }, + { + "label": "AntiheroPr", + "value": "3700" + }, + { + "label": "AlternateW", + "value": "3684" + }, + { + "label": "Acting", + "value": "250" + }, + { + "label": "Antihero", + "value": "25" + }, + { + "label": "Adventurer", + "value": "551" + }, + { + "label": "Aliens", + "value": "328" + }, + { + "label": "AbilitySte", + "value": "954" + }, + { + "label": "AncientTim", + "value": "3649" + }, + { + "label": "ArrangedMa", + "value": "87" + }, + { + "label": "AbsentPare", + "value": "3644" + }, + { + "label": "AdaptedtoM", + "value": "4580" + }, + { + "label": "Accelerate", + "value": "3717" + }, + { + "label": "athlete", + "value": "3050" + }, + { + "label": "Army", + "value": "542" + }, + { + "label": "Agedistrib", + "value": "4447" + }, + { + "label": "Assassins", + "value": "627" + }, + { + "label": "AgeProgres", + "value": "3707" + }, + { + "label": "Aristocrac", + "value": "232" + }, + { + "label": "AbusiveCha", + "value": "3653" + }, + { + "label": "AlternateH", + "value": "4325" + }, + { + "label": "Amnesia", + "value": "154" + }, + { + "label": "Adultery", + "value": "1003" + }, + { + "label": "Almighty", + "value": "3237" + }, + { + "label": "Anime", + "value": "905" + }, + { + "label": "abuse", + "value": "3312" + }, + { + "label": "ArtifactCr", + "value": "3872" + }, + { + "label": "Aggressive", + "value": "3808" + }, + { + "label": "ApatheticP", + "value": "3738" + }, + { + "label": "AdoptedChi", + "value": "3869" + }, + { + "label": "Angels", + "value": "1060" + }, + { + "label": "AdoptedPro", + "value": "3720" + }, + { + "label": "Appearance", + "value": "3709" + }, + { + "label": "AbandonedC", + "value": "3708" + }, + { + "label": "AgeRegress", + "value": "4126" + }, + { + "label": "AdaptedtoM", + "value": "3955" + }, + { + "label": "AncientSpi", + "value": "4224" + }, + { + "label": "Archery", + "value": "743" + }, + { + "label": "Anal", + "value": "926" + }, + { + "label": "AdaptedtoD", + "value": "4581" + }, + { + "label": "Anotherwor", + "value": "773" + }, + { + "label": "America", + "value": "3182" + }, + { + "label": "Advancedte", + "value": "793" + }, + { + "label": "Azeroth", + "value": "1232" + }, + { + "label": "AdaptedtoA", + "value": "4177" + }, + { + "label": "Agent", + "value": "3360" + }, + { + "label": "AdaptedtoG", + "value": "3975" + }, + { + "label": "Autism", + "value": "253" + }, + { + "label": "AnimalRear", + "value": "3994" + }, + { + "label": "ArmsDealer", + "value": "3654" + }, + { + "label": "Artists", + "value": "4591" + }, + { + "label": "Alpha", + "value": "172" + }, + { + "label": "Abandoned", + "value": "400" + }, + { + "label": "Afterthesh", + "value": "4416" + }, + { + "label": "ArtifactsC", + "value": "3725" + }, + { + "label": "Afunnyguy", + "value": "4408" + }, + { + "label": "Aristocrat", + "value": "4673" + }, + { + "label": "Angel", + "value": "792" + }, + { + "label": "Alternativ", + "value": "1317" + }, + { + "label": "Aresstream", + "value": "4542" + }, + { + "label": "AdaptedtoD", + "value": "4646" + }, + { + "label": "AwkwardPro", + "value": "4648" + }, + { + "label": "Androids", + "value": "4709" + }, + { + "label": "Adult", + "value": "924" + }, + { + "label": "Affair", + "value": "4653" + }, + { + "label": "ApartmentL", + "value": "3937" + }, + { + "label": "Anti-Japan", + "value": "4289" + }, + { + "label": "ArrayMage", + "value": "4537" + }, + { + "label": "Anti-Magic", + "value": "3986" + }, + { + "label": "AbusiveCha", + "value": "3874" + }, + { + "label": "airflow", + "value": "4378" + }, + { + "label": "AdaptedtoM", + "value": "4595" + }, + { + "label": "AdaptedtoM", + "value": "4719" + }, + { + "label": "AntiqueSho", + "value": "4777" + }, + { + "label": "AmusementP", + "value": "4818" + }, + { + "label": "Arknights", + "value": "4073" + }, + { + "label": "Assasins", + "value": "3905" + }, + { + "label": "Actors", + "value": "3999" + }, + { + "label": "Anti-heroP", + "value": "3995" + }, + { + "label": "Appearance", + "value": "3837" + }, + { + "label": "Androgynou", + "value": "3836" + }, + { + "label": "ArtifactsB", + "value": "3829" + }, + { + "label": "AnimalChar", + "value": "3778" + }, + { + "label": "AncientChi", + "value": "3768" + }, + { + "label": "Average-lo", + "value": "3923" + }, + { + "label": "Anti-MC", + "value": "4178" + }, + { + "label": "AzurLane", + "value": "3971" + }, + { + "label": "artificer", + "value": "4162" + }, + { + "label": "Anti-socia", + "value": "3699" + }, + { + "label": "ACGN", + "value": "4121" + }, + { + "label": "Artificial", + "value": "3732" + }, + { + "label": "archeology", + "value": "4329" + }, + { + "label": "ancientwil", + "value": "4375" + }, + { + "label": "ancienthis", + "value": "4445" + }, + { + "label": "Americaiss", + "value": "4516" + }, + { + "label": "Amatchmade", + "value": "4528" + }, + { + "label": "ambiguous", + "value": "4547" + }, + { + "label": "Apprentice", + "value": "4681" + }, + { + "label": "Award-winn", + "value": "4702" + }, + { + "label": "Assassin", + "value": "4705" + }, + { + "label": "armoredcit", + "value": "4720" + }, + { + "label": "Abyss", + "value": "4727" + }, + { + "label": "Animation", + "value": "4731" + }, + { + "label": "AnimationD", + "value": "4732" + }, + { + "label": "Adventurer", + "value": "4741" + }, + { + "label": "Americas", + "value": "4744" + }, + { + "label": "Aesthetic", + "value": "4761" + }, + { + "label": "Astrologer", + "value": "4836" + }, + { + "label": "Adaptedfro", + "value": "4841" + }, + { + "label": "Bdsm", + "value": "2736" + }, + { + "label": "Birth", + "value": "2741" + }, + { + "label": "BeautifulF", + "value": "3701" + }, + { + "label": "basketball", + "value": "3049" + }, + { + "label": "Bellyblack", + "value": "4209" + }, + { + "label": "bigbrain", + "value": "4229" + }, + { + "label": "BusinessMa", + "value": "3655" + }, + { + "label": "Blood", + "value": "2692" + }, + { + "label": "Blackening", + "value": "3046" + }, + { + "label": "BlackBelly", + "value": "3952" + }, + { + "label": "BeastCompa", + "value": "3741" + }, + { + "label": "Betrayal", + "value": "45" + }, + { + "label": "Businessme", + "value": "315" + }, + { + "label": "Bloodlines", + "value": "80" + }, + { + "label": "Beasts", + "value": "501" + }, + { + "label": "Boss", + "value": "123" + }, + { + "label": "BodyTemper", + "value": "4169" + }, + { + "label": "Behindthes", + "value": "4269" + }, + { + "label": "Buddhism", + "value": "492" + }, + { + "label": "Bleach", + "value": "1114" + }, + { + "label": "BasedonaMo", + "value": "3685" + }, + { + "label": "Brotherhoo", + "value": "353" + }, + { + "label": "bodyguard", + "value": "3507" + }, + { + "label": "BOSSflow", + "value": "4297" + }, + { + "label": "Baby", + "value": "317" + }, + { + "label": "BattleComp", + "value": "4022" + }, + { + "label": "Blackmail", + "value": "898" + }, + { + "label": "BattleAcad", + "value": "3751" + }, + { + "label": "Beauty", + "value": "373" + }, + { + "label": "Blacksmith", + "value": "705" + }, + { + "label": "beautifulh", + "value": "3792" + }, + { + "label": "Brainwashi", + "value": "988" + }, + { + "label": "Beastkin", + "value": "4058" + }, + { + "label": "BasedonaTV", + "value": "3767" + }, + { + "label": "BeastTamer", + "value": "1282" + }, + { + "label": "BrotherCom", + "value": "3773" + }, + { + "label": "Books", + "value": "3772" + }, + { + "label": "BickeringC", + "value": "4019" + }, + { + "label": "BrokenEnga", + "value": "4589" + }, + { + "label": "BasedonaVi", + "value": "4839" + }, + { + "label": "Beasttamin", + "value": "137" + }, + { + "label": "Biochip", + "value": "3629" + }, + { + "label": "Bullying", + "value": "3810" + }, + { + "label": "Blacklotus", + "value": "4404" + }, + { + "label": "Bl", + "value": "292" + }, + { + "label": "Bodyguards", + "value": "4593" + }, + { + "label": "Brother", + "value": "3358" + }, + { + "label": "BasedonanA", + "value": "4810" + }, + { + "label": "Badboy", + "value": "254" + }, + { + "label": "Bloodpumpi", + "value": "790" + }, + { + "label": "BodySwap", + "value": "4111" + }, + { + "label": "beastworld", + "value": "4348" + }, + { + "label": "BlindProta", + "value": "4611" + }, + { + "label": "BeautifulP", + "value": "3877" + }, + { + "label": "Bestiality", + "value": "4159" + }, + { + "label": "BloodManip", + "value": "4152" + }, + { + "label": "Booktransm", + "value": "205" + }, + { + "label": "Businesswo", + "value": "340" + }, + { + "label": "Bookworm", + "value": "3873" + }, + { + "label": "BE", + "value": "2913" + }, + { + "label": "Business", + "value": "3968" + }, + { + "label": "BusinessMa", + "value": "3969" + }, + { + "label": "Baseball", + "value": "4125" + }, + { + "label": "BungouStra", + "value": "4165" + }, + { + "label": "battleofwi", + "value": "4541" + }, + { + "label": "Bloodborne", + "value": "4074" + }, + { + "label": "BunguoStra", + "value": "4056" + }, + { + "label": "BTTH", + "value": "4029" + }, + { + "label": "Beast", + "value": "4025" + }, + { + "label": "Basket", + "value": "4115" + }, + { + "label": "BlindDates", + "value": "4142" + }, + { + "label": "Boss-Subor", + "value": "3686" + }, + { + "label": "Blackbelli", + "value": "4089" + }, + { + "label": "Blacklight", + "value": "4131" + }, + { + "label": "Biomass", + "value": "4130" + }, + { + "label": "Beautifull", + "value": "4098" + }, + { + "label": "Biochemist", + "value": "4401" + }, + { + "label": "Brainhole", + "value": "4501" + }, + { + "label": "Blinddate", + "value": "4509" + }, + { + "label": "black-bell", + "value": "4536" + }, + { + "label": "Butlers", + "value": "4594" + }, + { + "label": "Boxing", + "value": "4655" + }, + { + "label": "Billionair", + "value": "4680" + }, + { + "label": "Bulldozer", + "value": "4716" + }, + { + "label": "building", + "value": "4752" + }, + { + "label": "Bands", + "value": "4808" + }, + { + "label": "BasedonaVi", + "value": "4843" + }, + { + "label": "Cross", + "value": "2813" + }, + { + "label": "Comprehens", + "value": "2701" + }, + { + "label": "city", + "value": "3061" + }, + { + "label": "Campus", + "value": "438" + }, + { + "label": "Cultivatio", + "value": "31" + }, + { + "label": "calm", + "value": "2947" + }, + { + "label": "Cute", + "value": "161" + }, + { + "label": "Counteratt", + "value": "156" + }, + { + "label": "Cheats", + "value": "221" + }, + { + "label": "CalmProtag", + "value": "3726" + }, + { + "label": "Comedy", + "value": "82" + }, + { + "label": "CleverProt", + "value": "3753" + }, + { + "label": "Celebrity", + "value": "3199" + }, + { + "label": "clear", + "value": "2717" + }, + { + "label": "Conan", + "value": "2865" + }, + { + "label": "ComedicUnd", + "value": "3695" + }, + { + "label": "CunningPro", + "value": "3676" + }, + { + "label": "Celebritie", + "value": "305" + }, + { + "label": "comprehens", + "value": "2845" + }, + { + "label": "card", + "value": "2934" + }, + { + "label": "cautious", + "value": "2978" + }, + { + "label": "Childcare", + "value": "97" + }, + { + "label": "Chronology", + "value": "2766" + }, + { + "label": "Cooking", + "value": "43" + }, + { + "label": "clearthink", + "value": "4245" + }, + { + "label": "CollegeStr", + "value": "4285" + }, + { + "label": "ColdProtag", + "value": "3814" + }, + { + "label": "CautiousPr", + "value": "3710" + }, + { + "label": "ChatGroup", + "value": "1077" + }, + { + "label": "coach", + "value": "3051" + }, + { + "label": "cure", + "value": "3257" + }, + { + "label": "CosmicWars", + "value": "3754" + }, + { + "label": "ChatRooms", + "value": "3752" + }, + { + "label": "ColdLoveIn", + "value": "3886" + }, + { + "label": "CardGames", + "value": "3815" + }, + { + "label": "Cheat", + "value": "818" + }, + { + "label": "Courtyardh", + "value": "4512" + }, + { + "label": "CruelChara", + "value": "3658" + }, + { + "label": "Clones", + "value": "282" + }, + { + "label": "chef", + "value": "3314" + }, + { + "label": "ChinesePre", + "value": "1269" + }, + { + "label": "ChildProta", + "value": "3757" + }, + { + "label": "CharacterG", + "value": "3656" + }, + { + "label": "CharmingPr", + "value": "3733" + }, + { + "label": "Ceo", + "value": "111" + }, + { + "label": "ConfidentP", + "value": "3711" + }, + { + "label": "Contendfor", + "value": "4310" + }, + { + "label": "CollegeUni", + "value": "3833" + }, + { + "label": "Cthulhu", + "value": "2951" + }, + { + "label": "CaringProt", + "value": "3689" + }, + { + "label": "cannonfodd", + "value": "4228" + }, + { + "label": "ClanSectDe", + "value": "1278" + }, + { + "label": "collapse", + "value": "3246" + }, + { + "label": "CuteProtag", + "value": "3846" + }, + { + "label": "Crafting", + "value": "116" + }, + { + "label": "Conquer", + "value": "209" + }, + { + "label": "CuteChildr", + "value": "3816" + }, + { + "label": "Crossover", + "value": "943" + }, + { + "label": "CarefreePr", + "value": "3997" + }, + { + "label": "ClanBuildi", + "value": "3809" + }, + { + "label": "Contracts", + "value": "413" + }, + { + "label": "Chinesemed", + "value": "4307" + }, + { + "label": "Couple", + "value": "3471" + }, + { + "label": "CowardlyPr", + "value": "3698" + }, + { + "label": "Cyberpunk", + "value": "2825" + }, + { + "label": "ChaotangJi", + "value": "4336" + }, + { + "label": "ClingyLove", + "value": "4620" + }, + { + "label": "Crime", + "value": "75" + }, + { + "label": "ChildhoodF", + "value": "3900" + }, + { + "label": "CoupleGrow", + "value": "3813" + }, + { + "label": "Chinese", + "value": "4578" + }, + { + "label": "Cannibalis", + "value": "3841" + }, + { + "label": "ChildAbuse", + "value": "3657" + }, + { + "label": "Chefs", + "value": "4077" + }, + { + "label": "contractma", + "value": "4193" + }, + { + "label": "ChoiceSele", + "value": "1274" + }, + { + "label": "CuteStory", + "value": "3847" + }, + { + "label": "Conditiona", + "value": "3830" + }, + { + "label": "Corruption", + "value": "3949" + }, + { + "label": "Cross-dres", + "value": "4606" + }, + { + "label": "chief", + "value": "3541" + }, + { + "label": "chase", + "value": "3509" + }, + { + "label": "commander", + "value": "3391" + }, + { + "label": "ChildhoodL", + "value": "3793" + }, + { + "label": "Curses", + "value": "3987" + }, + { + "label": "CharacterD", + "value": "3758" + }, + { + "label": "cosplaystr", + "value": "4276" + }, + { + "label": "ciweimao", + "value": "4763" + }, + { + "label": "Cohabitati", + "value": "3745" + }, + { + "label": "ChinaEnter", + "value": "4511" + }, + { + "label": "Criminals", + "value": "3939" + }, + { + "label": "ChildhoodS", + "value": "3844" + }, + { + "label": "chasingwif", + "value": "4398" + }, + { + "label": "Contrastcu", + "value": "4414" + }, + { + "label": "ChildishPr", + "value": "3825" + }, + { + "label": "Crossing", + "value": "543" + }, + { + "label": "contract", + "value": "3520" + }, + { + "label": "ClumsyLove", + "value": "3855" + }, + { + "label": "captain", + "value": "3192" + }, + { + "label": "catchfast", + "value": "4369" + }, + { + "label": "ChildhoodP", + "value": "3744" + }, + { + "label": "Creatures", + "value": "3734" + }, + { + "label": "CampusLove", + "value": "3938" + }, + { + "label": "contractlo", + "value": "4195" + }, + { + "label": "ClassicXia", + "value": "4278" + }, + { + "label": "Crosstalka", + "value": "4521" + }, + { + "label": "ComingofAg", + "value": "4587" + }, + { + "label": "CuriousPro", + "value": "4639" + }, + { + "label": "Crossdress", + "value": "4695" + }, + { + "label": "CourtOffic", + "value": "4713" + }, + { + "label": "Cityurban", + "value": "4756" + }, + { + "label": "Conflictin", + "value": "3966" + }, + { + "label": "Civilizati", + "value": "4322" + }, + { + "label": "courtstrug", + "value": "4465" + }, + { + "label": "crossfarmi", + "value": "4467" + }, + { + "label": "Cousins", + "value": "4016" + }, + { + "label": "coolandhan", + "value": "4396" + }, + { + "label": "Concubine", + "value": "4421" + }, + { + "label": "cynicism", + "value": "4425" + }, + { + "label": "CP", + "value": "4441" + }, + { + "label": "Crush", + "value": "4491" + }, + { + "label": "chanceenco", + "value": "4498" + }, + { + "label": "Cunnilingu", + "value": "4730" + }, + { + "label": "Coma", + "value": "4789" + }, + { + "label": "ChuningMC", + "value": "4059" + }, + { + "label": "CareerOrie", + "value": "3959" + }, + { + "label": "Capitalist", + "value": "3893" + }, + { + "label": "Colonializ", + "value": "4008" + }, + { + "label": "ComplexFam", + "value": "3786" + }, + { + "label": "Charismati", + "value": "3770" + }, + { + "label": "CosmicHorr", + "value": "4079" + }, + { + "label": "ChainsawMa", + "value": "4173" + }, + { + "label": "Competitio", + "value": "4116" + }, + { + "label": "CleverProt", + "value": "3918" + }, + { + "label": "Chuunibyou", + "value": "3935" + }, + { + "label": "CleaverPro", + "value": "4181" + }, + { + "label": "codegeass", + "value": "4167" + }, + { + "label": "CruelMC", + "value": "4158" + }, + { + "label": "Club", + "value": "4091" + }, + { + "label": "CrazyProta", + "value": "4146" + }, + { + "label": "Contagonis", + "value": "4133" + }, + { + "label": "CunningPro", + "value": "4084" + }, + { + "label": "ChenHegao", + "value": "4132" + }, + { + "label": "Comic", + "value": "4087" + }, + { + "label": "College", + "value": "4092" + }, + { + "label": "Criminalin", + "value": "4368" + }, + { + "label": "Chaotang", + "value": "4395" + }, + { + "label": "cook", + "value": "4399" + }, + { + "label": "crowmouth", + "value": "4417" + }, + { + "label": "civilizati", + "value": "4452" + }, + { + "label": "chasingwif", + "value": "4459" + }, + { + "label": "commonerli", + "value": "4540" + }, + { + "label": "Cat", + "value": "4574" + }, + { + "label": "cunningfem", + "value": "4676" + }, + { + "label": "Calmdown", + "value": "4745" + }, + { + "label": "carpenter", + "value": "4755" + }, + { + "label": "Collection", + "value": "4778" + }, + { + "label": "Clubs", + "value": "4787" + }, + { + "label": "Co-Workers", + "value": "4803" + }, + { + "label": "Dining", + "value": "2767" + }, + { + "label": "DouluoCont", + "value": "4548" + }, + { + "label": "Develop", + "value": "2900" + }, + { + "label": "disguise", + "value": "2785" + }, + { + "label": "Demons", + "value": "36" + }, + { + "label": "Dark", + "value": "208" + }, + { + "label": "DouluoDalu", + "value": "952" + }, + { + "label": "Dragons", + "value": "418" + }, + { + "label": "DevotedLov", + "value": "3731" + }, + { + "label": "derivative", + "value": "3156" + }, + { + "label": "Detectives", + "value": "841" + }, + { + "label": "Doctors", + "value": "175" + }, + { + "label": "doctorstre", + "value": "4265" + }, + { + "label": "DotingLove", + "value": "3746" + }, + { + "label": "Discrimina", + "value": "349" + }, + { + "label": "Drama", + "value": "249" + }, + { + "label": "Datang", + "value": "3390" + }, + { + "label": "Dragon", + "value": "405" + }, + { + "label": "Depictions", + "value": "3713" + }, + { + "label": "Daqin", + "value": "3442" + }, + { + "label": "Dragonball", + "value": "1162" + }, + { + "label": "DaoCompreh", + "value": "4033" + }, + { + "label": "Dwarfs", + "value": "537" + }, + { + "label": "DotingPare", + "value": "3853" + }, + { + "label": "Daoism", + "value": "4034" + }, + { + "label": "DenseProta", + "value": "3791" + }, + { + "label": "DotingOlde", + "value": "4662" + }, + { + "label": "DetectiveC", + "value": "1145" + }, + { + "label": "DeathofLov", + "value": "3712" + }, + { + "label": "DemonLord", + "value": "3659" + }, + { + "label": "Devil", + "value": "608" + }, + { + "label": "Demon", + "value": "62" + }, + { + "label": "DiscipleTr", + "value": "1293" + }, + { + "label": "DND", + "value": "3428" + }, + { + "label": "divorce", + "value": "4494" + }, + { + "label": "Domineerin", + "value": "4476" + }, + { + "label": "DollsPuppe", + "value": "3794" + }, + { + "label": "DC", + "value": "1038" + }, + { + "label": "Destiny", + "value": "3943" + }, + { + "label": "DomesticAf", + "value": "4009" + }, + { + "label": "Dungeons", + "value": "4023" + }, + { + "label": "Demi-Human", + "value": "4183" + }, + { + "label": "Disguiseas", + "value": "4313" + }, + { + "label": "Dreams", + "value": "4066" + }, + { + "label": "Detective", + "value": "1068" + }, + { + "label": "DemonSlaye", + "value": "1115" + }, + { + "label": "Disabiliti", + "value": "4021" + }, + { + "label": "Doublebusi", + "value": "4237" + }, + { + "label": "Detectiver", + "value": "4332" + }, + { + "label": "DragonRide", + "value": "4018" + }, + { + "label": "Divination", + "value": "4063" + }, + { + "label": "Doctor", + "value": "339" + }, + { + "label": "Darkfantas", + "value": "698" + }, + { + "label": "Dangmei", + "value": "2894" + }, + { + "label": "DragonSlay", + "value": "3630" + }, + { + "label": "dedicated", + "value": "3537" + }, + { + "label": "DaoCompani", + "value": "4701" + }, + { + "label": "Dramatic", + "value": "510" + }, + { + "label": "Drugs", + "value": "3798" + }, + { + "label": "Doomsday", + "value": "768" + }, + { + "label": "Death", + "value": "3755" + }, + { + "label": "Druids", + "value": "4754" + }, + { + "label": "Dystopia", + "value": "806" + }, + { + "label": "Dotingwife", + "value": "4480" + }, + { + "label": "Director", + "value": "4000" + }, + { + "label": "DeadProtag", + "value": "3848" + }, + { + "label": "DeepLTrans", + "value": "1294" + }, + { + "label": "Delusions", + "value": "3936" + }, + { + "label": "doublebirt", + "value": "4314" + }, + { + "label": "Douluo", + "value": "3934" + }, + { + "label": "Doupo", + "value": "3889" + }, + { + "label": "DoupoBTTH", + "value": "3856" + }, + { + "label": "Determined", + "value": "3747" + }, + { + "label": "DishonestP", + "value": "3920" + }, + { + "label": "Dream", + "value": "4030" + }, + { + "label": "Disfigurem", + "value": "4797" + }, + { + "label": "DungeonMas", + "value": "4805" + }, + { + "label": "DarkPower", + "value": "4042" + }, + { + "label": "DemonicCul", + "value": "4017" + }, + { + "label": "DumbProtag", + "value": "3967" + }, + { + "label": "DarkDeatho", + "value": "3797" + }, + { + "label": "Doraemon", + "value": "4174" + }, + { + "label": "DifferentS", + "value": "4188" + }, + { + "label": "dungeon", + "value": "4041" + }, + { + "label": "DemonPower", + "value": "4179" + }, + { + "label": "DragonPowe", + "value": "4180" + }, + { + "label": "directdaug", + "value": "4343" + }, + { + "label": "DouroConti", + "value": "4345" + }, + { + "label": "D", + "value": "4560" + }, + { + "label": "Danmei", + "value": "4584" + }, + { + "label": "DiscipleLo", + "value": "4682" + }, + { + "label": "Dominator", + "value": "4693" + }, + { + "label": "Dancers", + "value": "4733" + }, + { + "label": "Distrustfu", + "value": "4826" + }, + { + "label": "Depression", + "value": "4846" + }, + { + "label": "Debts", + "value": "4848" + }, + { + "label": "Entertainm", + "value": "803" + }, + { + "label": "Eschatolog", + "value": "2723" + }, + { + "label": "everydayla", + "value": "4260" + }, + { + "label": "Evolution", + "value": "6" + }, + { + "label": "Elvish", + "value": "2828" + }, + { + "label": "Exotic", + "value": "2693" + }, + { + "label": "EarlyRoman", + "value": "3667" + }, + { + "label": "Empress", + "value": "3066" + }, + { + "label": "Elves", + "value": "466" + }, + { + "label": "EvilGods", + "value": "3660" + }, + { + "label": "EvilProtag", + "value": "3632" + }, + { + "label": "EyePowers", + "value": "3677" + }, + { + "label": "ElementalM", + "value": "3647" + }, + { + "label": "emperor", + "value": "3461" + }, + { + "label": "EvilOrgani", + "value": "3631" + }, + { + "label": "EuropeanAm", + "value": "3915" + }, + { + "label": "Eatchicken", + "value": "4268" + }, + { + "label": "Empires", + "value": "406" + }, + { + "label": "EarthInvas", + "value": "3645" + }, + { + "label": "e-Sports", + "value": "3957" + }, + { + "label": "evildoer", + "value": "3254" + }, + { + "label": "EvilReligi", + "value": "3687" + }, + { + "label": "Europe", + "value": "3198" + }, + { + "label": "EasyGoingL", + "value": "3748" + }, + { + "label": "EideticMem", + "value": "3870" + }, + { + "label": "Economics", + "value": "3963" + }, + { + "label": "Ecchi", + "value": "847" + }, + { + "label": "EnemiesBec", + "value": "3756" + }, + { + "label": "Eunuch", + "value": "3413" + }, + { + "label": "Exorcism", + "value": "1020" + }, + { + "label": "Exposurefl", + "value": "4505" + }, + { + "label": "EnemiesBec", + "value": "3763" + }, + { + "label": "Eat", + "value": "3468" + }, + { + "label": "Egoist", + "value": "564" + }, + { + "label": "Engage", + "value": "3300" + }, + { + "label": "easternfan", + "value": "3650" + }, + { + "label": "Enemiestol", + "value": "375" + }, + { + "label": "Experience", + "value": "1270" + }, + { + "label": "Escrow", + "value": "3364" + }, + { + "label": "enemy", + "value": "3248" + }, + { + "label": "Engineer", + "value": "4630" + }, + { + "label": "Expressflo", + "value": "4362" + }, + { + "label": "emperorstr", + "value": "4387" + }, + { + "label": "Engagement", + "value": "4647" + }, + { + "label": "Exhaustion", + "value": "2886" + }, + { + "label": "Episodic", + "value": "4623" + }, + { + "label": "Effort", + "value": "4477" + }, + { + "label": "Emotionall", + "value": "3739" + }, + { + "label": "easyandfun", + "value": "4499" + }, + { + "label": "Enlightenm", + "value": "4799" + }, + { + "label": "Editors", + "value": "4067" + }, + { + "label": "EvilOrgani", + "value": "4052" + }, + { + "label": "EldestSist", + "value": "3910" + }, + { + "label": "Education", + "value": "3878" + }, + { + "label": "Empire", + "value": "4010" + }, + { + "label": "Exorcist", + "value": "4001" + }, + { + "label": "EvilGod", + "value": "3962" + }, + { + "label": "Easygoingp", + "value": "4090" + }, + { + "label": "Eccentricp", + "value": "4100" + }, + { + "label": "emotion", + "value": "4496" + }, + { + "label": "escapemarr", + "value": "4517" + }, + { + "label": "Esper", + "value": "4571" + }, + { + "label": "Elf", + "value": "4711" + }, + { + "label": "elemental", + "value": "4746" + }, + { + "label": "ElderlyPro", + "value": "4834" + }, + { + "label": "Faloo", + "value": "1267" + }, + { + "label": "Fantasy", + "value": "11" + }, + { + "label": "Fumino", + "value": "2760" + }, + { + "label": "Fanfiction", + "value": "527" + }, + { + "label": "Fan-fictio", + "value": "939" + }, + { + "label": "fastwear", + "value": "4255" + }, + { + "label": "Funny", + "value": "2936" + }, + { + "label": "FemaleProt", + "value": "44" + }, + { + "label": "Farming", + "value": "196" + }, + { + "label": "Fantasybra", + "value": "4430" + }, + { + "label": "Fastpaced", + "value": "619" + }, + { + "label": "Football", + "value": "1340" + }, + { + "label": "flood", + "value": "2788" + }, + { + "label": "fanqienove", + "value": "1339" + }, + { + "label": "FastCultiv", + "value": "3737" + }, + { + "label": "FantasyWor", + "value": "3703" + }, + { + "label": "Futuristic", + "value": "3947" + }, + { + "label": "futureworl", + "value": "4374" + }, + { + "label": "flowercare", + "value": "4217" + }, + { + "label": "femalematc", + "value": "4270" + }, + { + "label": "Family", + "value": "381" + }, + { + "label": "Fightagain", + "value": "4340" + }, + { + "label": "FamousProt", + "value": "3728" + }, + { + "label": "Friendship", + "value": "128" + }, + { + "label": "FantasyCre", + "value": "3736" + }, + { + "label": "FastLearne", + "value": "3749" + }, + { + "label": "FamilialLo", + "value": "3834" + }, + { + "label": "FantasyMag", + "value": "3702" + }, + { + "label": "Faceslappi", + "value": "67" + }, + { + "label": "Firearms", + "value": "324" + }, + { + "label": "foodie", + "value": "3362" + }, + { + "label": "fashionabl", + "value": "2838" + }, + { + "label": "fairies", + "value": "3203" + }, + { + "label": "Fan", + "value": "2802" + }, + { + "label": "Firstlove", + "value": "386" + }, + { + "label": "Fairytail", + "value": "1246" + }, + { + "label": "Flourishin", + "value": "4358" + }, + { + "label": "Farmer", + "value": "3398" + }, + { + "label": "Fishing", + "value": "3184" + }, + { + "label": "finepoint", + "value": "4407" + }, + { + "label": "FamilyBusi", + "value": "3852" + }, + { + "label": "Fellatio", + "value": "3804" + }, + { + "label": "FamilyConf", + "value": "4614" + }, + { + "label": "FoodWars!", + "value": "1263" + }, + { + "label": "Fanaticism", + "value": "958" + }, + { + "label": "flirtatiou", + "value": "3417" + }, + { + "label": "fullflow", + "value": "4312" + }, + { + "label": "FutureCivi", + "value": "3919" + }, + { + "label": "Future", + "value": "673" + }, + { + "label": "FattoFit", + "value": "3876" + }, + { + "label": "Fanfic", + "value": "652" + }, + { + "label": "FemaleMast", + "value": "3799" + }, + { + "label": "Femalelead", + "value": "216" + }, + { + "label": "finance", + "value": "1321" + }, + { + "label": "FoxSpirits", + "value": "3803" + }, + { + "label": "ForcedMarr", + "value": "3985" + }, + { + "label": "Feelgood", + "value": "291" + }, + { + "label": "Furutake", + "value": "3183" + }, + { + "label": "fox", + "value": "3265" + }, + { + "label": "FatedLover", + "value": "4780" + }, + { + "label": "Fatedlove", + "value": "422" + }, + { + "label": "FamousPare", + "value": "3735" + }, + { + "label": "FearlessPr", + "value": "3639" + }, + { + "label": "FleetBattl", + "value": "3953" + }, + { + "label": "Femalepres", + "value": "4455" + }, + { + "label": "FriendsBec", + "value": "4590" + }, + { + "label": "FallenNobi", + "value": "3887" + }, + { + "label": "FallenAnge", + "value": "4123" + }, + { + "label": "folksuspen", + "value": "4367" + }, + { + "label": "fantasyfan", + "value": "4460" + }, + { + "label": "FantasyFai", + "value": "4463" + }, + { + "label": "Filmempero", + "value": "4483" + }, + { + "label": "Fault", + "value": "4524" + }, + { + "label": "FatProtago", + "value": "4788" + }, + { + "label": "FengShui", + "value": "4831" + }, + { + "label": "Forcedinto", + "value": "4065" + }, + { + "label": "Futanari", + "value": "3879" + }, + { + "label": "Food", + "value": "4049" + }, + { + "label": "femalehono", + "value": "4394" + }, + { + "label": "fantasyrom", + "value": "4493" + }, + { + "label": "非洲", + "value": "4544" + }, + { + "label": "FormerHero", + "value": "4800" + }, + { + "label": "FemaleMC", + "value": "4060" + }, + { + "label": "First-time", + "value": "3795" + }, + { + "label": "FanFicton", + "value": "4078" + }, + { + "label": "FemaleFigh", + "value": "4109" + }, + { + "label": "FateSeries", + "value": "4108" + }, + { + "label": "FemaletoMa", + "value": "4182" + }, + { + "label": "ForgetfulP", + "value": "3964" + }, + { + "label": "familylife", + "value": "4106" + }, + { + "label": "Forensic", + "value": "4436" + }, + { + "label": "filmandtel", + "value": "4438" + }, + { + "label": "fighting", + "value": "4470" + }, + { + "label": "fiercewife", + "value": "4486" + }, + { + "label": "房地产", + "value": "4545" + }, + { + "label": "ForcedLivi", + "value": "4605" + }, + { + "label": "France", + "value": "4692" + }, + { + "label": "FastGrowth", + "value": "4696" + }, + { + "label": "fasttravel", + "value": "4738" + }, + { + "label": "fqloo", + "value": "4766" + }, + { + "label": "Flashbacks", + "value": "4791" + }, + { + "label": "Familiars", + "value": "4823" + }, + { + "label": "GameElemen", + "value": "81" + }, + { + "label": "Giant", + "value": "3267" + }, + { + "label": "grudges", + "value": "2743" + }, + { + "label": "Genius", + "value": "113" + }, + { + "label": "gamealien", + "value": "4227" + }, + { + "label": "Gongdou", + "value": "2718" + }, + { + "label": "GeniusProt", + "value": "3714" + }, + { + "label": "Gangster", + "value": "3030" + }, + { + "label": "gameanime", + "value": "4432" + }, + { + "label": "Gintama", + "value": "2703" + }, + { + "label": "Gaming", + "value": "2910" + }, + { + "label": "Grandpa", + "value": "3313" + }, + { + "label": "Games", + "value": "2855" + }, + { + "label": "GodlyPower", + "value": "3669" + }, + { + "label": "GodProtago", + "value": "3729" + }, + { + "label": "Gods", + "value": "173" + }, + { + "label": "Greenplum", + "value": "4205" + }, + { + "label": "GodofWar", + "value": "4198" + }, + { + "label": "geniusflow", + "value": "4263" + }, + { + "label": "GameRankin", + "value": "3932" + }, + { + "label": "Ghosts", + "value": "285" + }, + { + "label": "Goldfinger", + "value": "4238" + }, + { + "label": "Gamers", + "value": "159" + }, + { + "label": "Grassroots", + "value": "4290" + }, + { + "label": "GatetoAnot", + "value": "3683" + }, + { + "label": "Group", + "value": "3149" + }, + { + "label": "Geeky", + "value": "3222" + }, + { + "label": "GeneticMod", + "value": "3661" + }, + { + "label": "grouppet", + "value": "4349" + }, + { + "label": "Game", + "value": "607" + }, + { + "label": "gene", + "value": "3330" + }, + { + "label": "Goddesses", + "value": "586" + }, + { + "label": "Gunfighter", + "value": "706" + }, + { + "label": "Gameproduc", + "value": "4251" + }, + { + "label": "Gore", + "value": "4071" + }, + { + "label": "GeneModifi", + "value": "1283" + }, + { + "label": "Guoshu", + "value": "3354" + }, + { + "label": "God-levelf", + "value": "4246" + }, + { + "label": "gameplayer", + "value": "4305" + }, + { + "label": "Gangs", + "value": "4153" + }, + { + "label": "Guilds", + "value": "4012" + }, + { + "label": "gangfeud", + "value": "4212" + }, + { + "label": "Genderbend", + "value": "495" + }, + { + "label": "groupwear", + "value": "4406" + }, + { + "label": "Gaowuworld", + "value": "4450" + }, + { + "label": "Gotoschool", + "value": "4301" + }, + { + "label": "Generals", + "value": "4006" + }, + { + "label": "Greatgod", + "value": "4299" + }, + { + "label": "Gatevalve", + "value": "4373" + }, + { + "label": "Goblins", + "value": "4143" + }, + { + "label": "geneticwar", + "value": "4424" + }, + { + "label": "GamingE-Sp", + "value": "4626" + }, + { + "label": "Groupspoil", + "value": "140" + }, + { + "label": "grimReaper", + "value": "4403" + }, + { + "label": "God", + "value": "4419" + }, + { + "label": "Go", + "value": "4573" + }, + { + "label": "GameElemen", + "value": "3941" + }, + { + "label": "Genshinimp", + "value": "4157" + }, + { + "label": "Goddess", + "value": "4096" + }, + { + "label": "Gundam", + "value": "4144" + }, + { + "label": "GoldenFing", + "value": "4099" + }, + { + "label": "Grinding", + "value": "4585" + }, + { + "label": "Gambling", + "value": "4775" + }, + { + "label": "GuardianRe", + "value": "3904" + }, + { + "label": "GenderRole", + "value": "3911" + }, + { + "label": "Genderless", + "value": "3921" + }, + { + "label": "Godzilla", + "value": "4134" + }, + { + "label": "GodlyProta", + "value": "3965" + }, + { + "label": "Geass", + "value": "4107" + }, + { + "label": "God-humanR", + "value": "3688" + }, + { + "label": "Gameanchor", + "value": "4522" + }, + { + "label": "growingup", + "value": "4550" + }, + { + "label": "General", + "value": "4563" + }, + { + "label": "girlfriend", + "value": "4664" + }, + { + "label": "Genies", + "value": "4798" + }, + { + "label": "Harem", + "value": "9" + }, + { + "label": "Hotblood", + "value": "4225" + }, + { + "label": "Haveasofts", + "value": "4202" + }, + { + "label": "HandsomeMa", + "value": "3704" + }, + { + "label": "HP", + "value": "2721" + }, + { + "label": "happyenemy", + "value": "4201" + }, + { + "label": "Hunter", + "value": "1324" + }, + { + "label": "HE", + "value": "2912" + }, + { + "label": "Heavensand", + "value": "4431" + }, + { + "label": "history", + "value": "3319" + }, + { + "label": "Hikusei", + "value": "2869" + }, + { + "label": "HarryPotte", + "value": "945" + }, + { + "label": "HidingTrue", + "value": "3743" + }, + { + "label": "Historical", + "value": "4518" + }, + { + "label": "HidingTrue", + "value": "3742" + }, + { + "label": "Heroes", + "value": "64" + }, + { + "label": "Historical", + "value": "225" + }, + { + "label": "HiddenAbil", + "value": "3835" + }, + { + "label": "Hunters", + "value": "955" + }, + { + "label": "Hero", + "value": "1318" + }, + { + "label": "Highcold", + "value": "4259" + }, + { + "label": "Highiq", + "value": "182" + }, + { + "label": "HumanExper", + "value": "3663" + }, + { + "label": "hacker", + "value": "3404" + }, + { + "label": "Hackers", + "value": "4024" + }, + { + "label": "Heartwarmi", + "value": "4612" + }, + { + "label": "HunterXHun", + "value": "1161" + }, + { + "label": "HumanoidPr", + "value": "3842" + }, + { + "label": "horror", + "value": "1022" + }, + { + "label": "HeavenlyTr", + "value": "3802" + }, + { + "label": "Hell", + "value": "3662" + }, + { + "label": "hardsci-fi", + "value": "4402" + }, + { + "label": "Handjob", + "value": "3940" + }, + { + "label": "Healing", + "value": "387" + }, + { + "label": "HarshTrain", + "value": "3679" + }, + { + "label": "Horor", + "value": "4757" + }, + { + "label": "Hypnotism", + "value": "4149" + }, + { + "label": "HiddenIden", + "value": "4093" + }, + { + "label": "hoardsuppl", + "value": "4514" + }, + { + "label": "Hospital", + "value": "4776" + }, + { + "label": "HonkaiImpa", + "value": "4048" + }, + { + "label": "Heaven", + "value": "3958" + }, + { + "label": "HandsomePr", + "value": "3912" + }, + { + "label": "Healers", + "value": "3838" + }, + { + "label": "HatedProta", + "value": "3800" + }, + { + "label": "Humbleboy", + "value": "4381" + }, + { + "label": "housegirl", + "value": "4556" + }, + { + "label": "HumanWeapo", + "value": "4608" + }, + { + "label": "HiddenTrue", + "value": "4657" + }, + { + "label": "HonestProt", + "value": "4792" + }, + { + "label": "HxH", + "value": "3960" + }, + { + "label": "Hogwarts", + "value": "3930" + }, + { + "label": "Human-Nonh", + "value": "3759" + }, + { + "label": "Heroesinvi", + "value": "4241" + }, + { + "label": "heroinecut", + "value": "4449" + }, + { + "label": "HidingTrue", + "value": "4691" + }, + { + "label": "Herbalist", + "value": "4794" + }, + { + "label": "HumanExper", + "value": "4061" + }, + { + "label": "Hollywood", + "value": "4002" + }, + { + "label": "Hardworkin", + "value": "4044" + }, + { + "label": "Hot-bloode", + "value": "3777" + }, + { + "label": "Harem-seek", + "value": "3678" + }, + { + "label": "Hard-Worki", + "value": "3670" + }, + { + "label": "Head", + "value": "4338" + }, + { + "label": "hiddenmarr", + "value": "4492" + }, + { + "label": "婚姻", + "value": "4535" + }, + { + "label": "holymonk", + "value": "4554" + }, + { + "label": "Half-human", + "value": "4640" + }, + { + "label": "HandsomeMa", + "value": "4734" + }, + { + "label": "HelpfulPro", + "value": "4847" + }, + { + "label": "Interstell", + "value": "830" + }, + { + "label": "Invincible", + "value": "4219" + }, + { + "label": "InfiniteFl", + "value": "4135" + }, + { + "label": "Invincible", + "value": "563" + }, + { + "label": "IQOnline", + "value": "4234" + }, + { + "label": "Infrastruc", + "value": "2851" + }, + { + "label": "Immortals", + "value": "388" + }, + { + "label": "Isekai", + "value": "114" + }, + { + "label": "inlove", + "value": "4273" + }, + { + "label": "Industrial", + "value": "16" + }, + { + "label": "infatuatio", + "value": "3253" + }, + { + "label": "Island", + "value": "3326" + }, + { + "label": "industry", + "value": "3375" + }, + { + "label": "ImperialHa", + "value": "3764" + }, + { + "label": "ideologica", + "value": "4294" + }, + { + "label": "Inspiratio", + "value": "2804" + }, + { + "label": "Immortal", + "value": "170" + }, + { + "label": "Incest", + "value": "229" + }, + { + "label": "Imperialex", + "value": "4361" + }, + { + "label": "Insects", + "value": "3924" + }, + { + "label": "Investigat", + "value": "3972" + }, + { + "label": "Infrastruc", + "value": "4376" + }, + { + "label": "ImmortalEm", + "value": "4525" + }, + { + "label": "Inheritanc", + "value": "4634" + }, + { + "label": "Inuyasha", + "value": "3982" + }, + { + "label": "Inscriptio", + "value": "4724" + }, + { + "label": "Interdimen", + "value": "3640" + }, + { + "label": "IdentityCr", + "value": "4597" + }, + { + "label": "Investigat", + "value": "4652" + }, + { + "label": "Interestel", + "value": "4667" + }, + { + "label": "imperialco", + "value": "4687" + }, + { + "label": "Inferiorit", + "value": "4790" + }, + { + "label": "Interconne", + "value": "4833" + }, + { + "label": "JianBao", + "value": "4422" + }, + { + "label": "Japanese", + "value": "2822" + }, + { + "label": "Junjie", + "value": "2849" + }, + { + "label": "家族", + "value": "3144" + }, + { + "label": "Jianghugri", + "value": "4204" + }, + { + "label": "氪金", + "value": "4284" + }, + { + "label": "JackofAllT", + "value": "3750" + }, + { + "label": "金融", + "value": "3434" + }, + { + "label": "JOJO", + "value": "2868" + }, + { + "label": "Journeytot", + "value": "1301" + }, + { + "label": "Jealousy", + "value": "4654" + }, + { + "label": "JujutsuKai", + "value": "3984" + }, + { + "label": "JojoBizarr", + "value": "3983" + }, + { + "label": "JoJo&03", + "value": "4170" + }, + { + "label": "JackieChan", + "value": "4175" + }, + { + "label": "JinYiwei", + "value": "4389" + }, + { + "label": "教授", + "value": "4487" + }, + { + "label": "Josei", + "value": "4688" + }, + { + "label": "恐怖", + "value": "2757" + }, + { + "label": "killdecisi", + "value": "4233" + }, + { + "label": "Kingdombui", + "value": "12" + }, + { + "label": "空间", + "value": "2901" + }, + { + "label": "knight", + "value": "3180" + }, + { + "label": "kendo", + "value": "3059" + }, + { + "label": "Kingdoms", + "value": "541" + }, + { + "label": "KoreanNove", + "value": "1289" + }, + { + "label": "KingofSold", + "value": "4221" + }, + { + "label": "Knights", + "value": "674" + }, + { + "label": "Killer", + "value": "795" + }, + { + "label": "Koi", + "value": "3431" + }, + { + "label": "Kindness", + "value": "3264" + }, + { + "label": "KindLoveIn", + "value": "4774" + }, + { + "label": "Kidnapping", + "value": "4784" + }, + { + "label": "KingdomsKn", + "value": "4068" + }, + { + "label": "KindProtag", + "value": "4085" + }, + { + "label": "KnightsLev", + "value": "4703" + }, + { + "label": "love", + "value": "322" + }, + { + "label": "livestream", + "value": "2763" + }, + { + "label": "legend", + "value": "2684" + }, + { + "label": "Lol", + "value": "3035" + }, + { + "label": "Livetext", + "value": "4230" + }, + { + "label": "Levelup", + "value": "53" + }, + { + "label": "Lottery", + "value": "983" + }, + { + "label": "Livebroadc", + "value": "1209" + }, + { + "label": "LevelSyste", + "value": "3680" + }, + { + "label": "LoyalSubor", + "value": "3681" + }, + { + "label": "LateRomanc", + "value": "3651" + }, + { + "label": "LoveatFirs", + "value": "640" + }, + { + "label": "LuckyProta", + "value": "3730" + }, + { + "label": "Long", + "value": "2954" + }, + { + "label": "Leadership", + "value": "3946" + }, + { + "label": "Lolicon", + "value": "949" + }, + { + "label": "Life", + "value": "3457" + }, + { + "label": "Low-keyPro", + "value": "4015" + }, + { + "label": "layoutflow", + "value": "4283" + }, + { + "label": "Loveeachot", + "value": "4354" + }, + { + "label": "lovebefore", + "value": "4192" + }, + { + "label": "leadthemai", + "value": "4248" + }, + { + "label": "LackofComm", + "value": "3787" + }, + { + "label": "lootflow", + "value": "4243" + }, + { + "label": "LazyProtag", + "value": "3765" + }, + { + "label": "Loli", + "value": "3372" + }, + { + "label": "loyaldog", + "value": "4344" + }, + { + "label": "Lightnovel", + "value": "22" + }, + { + "label": "LordGod", + "value": "4347" + }, + { + "label": "LonerProta", + "value": "3788" + }, + { + "label": "lastwear", + "value": "4405" + }, + { + "label": "LordoftheM", + "value": "3989" + }, + { + "label": "lady", + "value": "3496" + }, + { + "label": "Lovetriang", + "value": "374" + }, + { + "label": "ListCreati", + "value": "1271" + }, + { + "label": "Lawyers", + "value": "4838" + }, + { + "label": "LitRPG", + "value": "659" + }, + { + "label": "LostCivili", + "value": "4020" + }, + { + "label": "LivingAlon", + "value": "3865" + }, + { + "label": "LongSepara", + "value": "4184" + }, + { + "label": "LoversReun", + "value": "4690" + }, + { + "label": "Legends", + "value": "4005" + }, + { + "label": "LiaoZhai", + "value": "4286" + }, + { + "label": "Littlesold", + "value": "4320" + }, + { + "label": "Longevity", + "value": "4327" + }, + { + "label": "Leftgirl", + "value": "4410" + }, + { + "label": "lawyer", + "value": "4495" + }, + { + "label": "luckybag", + "value": "4555" + }, + { + "label": "Lovecomedy", + "value": "4566" + }, + { + "label": "LoveRivals", + "value": "4635" + }, + { + "label": "LimitedLif", + "value": "4644" + }, + { + "label": "LeagueofLe", + "value": "3885" + }, + { + "label": "LoyalProta", + "value": "4027" + }, + { + "label": "LoveIntere", + "value": "3862" + }, + { + "label": "lifeanddea", + "value": "4530" + }, + { + "label": "LoyalSurbo", + "value": "4699" + }, + { + "label": "LowKeyProt", + "value": "4706" + }, + { + "label": "LoveTriang", + "value": "4812" + }, + { + "label": "Long-dista", + "value": "4830" + }, + { + "label": "MaleProtag", + "value": "1" + }, + { + "label": "Manage", + "value": "2935" + }, + { + "label": "Magic", + "value": "4" + }, + { + "label": "Makemoney", + "value": "4302" + }, + { + "label": "Mech", + "value": "2732" + }, + { + "label": "Marvel", + "value": "901" + }, + { + "label": "Mature", + "value": "576" + }, + { + "label": "metaphysic", + "value": "2797" + }, + { + "label": "Modern", + "value": "60" + }, + { + "label": "Misunderst", + "value": "65" + }, + { + "label": "ModernDay", + "value": "3705" + }, + { + "label": "Martialart", + "value": "720" + }, + { + "label": "Mystery", + "value": "112" + }, + { + "label": "Monsters", + "value": "379" + }, + { + "label": "Misunderst", + "value": "34" + }, + { + "label": "Military", + "value": "148" + }, + { + "label": "MagicalSpa", + "value": "3779" + }, + { + "label": "moderncomp", + "value": "4222" + }, + { + "label": "mouthgun", + "value": "4277" + }, + { + "label": "Mage", + "value": "3187" + }, + { + "label": "Marriage", + "value": "56" + }, + { + "label": "ModernKnow", + "value": "3888" + }, + { + "label": "MultiplePO", + "value": "3664" + }, + { + "label": "Modernfant", + "value": "482" + }, + { + "label": "magician", + "value": "3505" + }, + { + "label": "Mythology", + "value": "3781" + }, + { + "label": "MarvelUniv", + "value": "3697" + }, + { + "label": "Makecompla", + "value": "4264" + }, + { + "label": "Multipleid", + "value": "57" + }, + { + "label": "MedicalKno", + "value": "4120" + }, + { + "label": "MengBao", + "value": "4194" + }, + { + "label": "Movies", + "value": "3822" + }, + { + "label": "MultipleRe", + "value": "3672" + }, + { + "label": "MonsterTam", + "value": "3635" + }, + { + "label": "MMORPG", + "value": "91" + }, + { + "label": "Malegod", + "value": "4315" + }, + { + "label": "MysterySol", + "value": "3863" + }, + { + "label": "Munchkin", + "value": "4565" + }, + { + "label": "MagicBeast", + "value": "3721" + }, + { + "label": "MythicalBe", + "value": "3780" + }, + { + "label": "ModernWorl", + "value": "3706" + }, + { + "label": "MatureProt", + "value": "4037" + }, + { + "label": "MoneyGrubb", + "value": "4035" + }, + { + "label": "MartialSpi", + "value": "3722" + }, + { + "label": "MagicalTec", + "value": "3922" + }, + { + "label": "MingDynast", + "value": "4326" + }, + { + "label": "MaletoFema", + "value": "3871" + }, + { + "label": "Mysterious", + "value": "3801" + }, + { + "label": "malematch", + "value": "4337" + }, + { + "label": "Music", + "value": "385" + }, + { + "label": "Manvs.Wild", + "value": "4341" + }, + { + "label": "Mensao", + "value": "3233" + }, + { + "label": "Mutations", + "value": "3981" + }, + { + "label": "Mecha", + "value": "672" + }, + { + "label": "MiddleAges", + "value": "4568" + }, + { + "label": "MultiplePr", + "value": "4007" + }, + { + "label": "Monk", + "value": "3352" + }, + { + "label": "MultipleTi", + "value": "3980" + }, + { + "label": "MagicForma", + "value": "4582" + }, + { + "label": "Mafia", + "value": "240" + }, + { + "label": "Mother-in-", + "value": "2848" + }, + { + "label": "Medieval", + "value": "3917" + }, + { + "label": "MaleYander", + "value": "3851" + }, + { + "label": "Myth", + "value": "488" + }, + { + "label": "makefine", + "value": "4239" + }, + { + "label": "MyHeroAcad", + "value": "1286" + }, + { + "label": "Mercenarie", + "value": "3839" + }, + { + "label": "Mentor", + "value": "3386" + }, + { + "label": "MindContro", + "value": "3634" + }, + { + "label": "Masterflow", + "value": "4254" + }, + { + "label": "MobileGame", + "value": "4262" + }, + { + "label": "Mpreg", + "value": "4601" + }, + { + "label": "Mutation", + "value": "737" + }, + { + "label": "MutatedCre", + "value": "4629" + }, + { + "label": "Management", + "value": "4637" + }, + { + "label": "Murders", + "value": "3880" + }, + { + "label": "Maids", + "value": "126" + }, + { + "label": "Masturbati", + "value": "3832" + }, + { + "label": "MobProtago", + "value": "4613" + }, + { + "label": "Merchants", + "value": "4651" + }, + { + "label": "Murder", + "value": "3902" + }, + { + "label": "Marysue", + "value": "332" + }, + { + "label": "ManlyGayCo", + "value": "3828" + }, + { + "label": "magicalgir", + "value": "4151" + }, + { + "label": "militaryma", + "value": "4210" + }, + { + "label": "modernroma", + "value": "4437" + }, + { + "label": "magicschoo", + "value": "4464" + }, + { + "label": "marrybycha", + "value": "4490" + }, + { + "label": "mortalflow", + "value": "4500" + }, + { + "label": "MonsterGir", + "value": "4782" + }, + { + "label": "Multiverse", + "value": "4064" + }, + { + "label": "MultipleTr", + "value": "3849" + }, + { + "label": "Masochisti", + "value": "3831" + }, + { + "label": "MonsterSoc", + "value": "3671" + }, + { + "label": "Marriageof", + "value": "4189" + }, + { + "label": "Medical", + "value": "4186" + }, + { + "label": "MaleLead", + "value": "4145" + }, + { + "label": "Matriarchy", + "value": "4128" + }, + { + "label": "Mindreadin", + "value": "4351" + }, + { + "label": "Miss", + "value": "4444" + }, + { + "label": "Mozun", + "value": "4553" + }, + { + "label": "miser", + "value": "4559" + }, + { + "label": "MultiplePe", + "value": "3901" + }, + { + "label": "Master-Ser", + "value": "3916" + }, + { + "label": "Mysterious", + "value": "3928" + }, + { + "label": "MartialSpi", + "value": "4043" + }, + { + "label": "MovieDirec", + "value": "3894" + }, + { + "label": "MaleMain-l", + "value": "4013" + }, + { + "label": "Monogamy", + "value": "4003" + }, + { + "label": "MultipleMo", + "value": "3996" + }, + { + "label": "MultipleTr", + "value": "3990" + }, + { + "label": "Master-Dis", + "value": "3760" + }, + { + "label": "Manipulati", + "value": "3633" + }, + { + "label": "MaleMc", + "value": "4112" + }, + { + "label": "MultipleBo", + "value": "4062" + }, + { + "label": "Ministryof", + "value": "4113" + }, + { + "label": "MagicalBat", + "value": "4110" + }, + { + "label": "Mimicry", + "value": "4136" + }, + { + "label": "MultipleWo", + "value": "4137" + }, + { + "label": "Morallessp", + "value": "4147" + }, + { + "label": "manyfemale", + "value": "4214" + }, + { + "label": "modernmyst", + "value": "4392" + }, + { + "label": "Mysterious", + "value": "4393" + }, + { + "label": "magicweapo", + "value": "4502" + }, + { + "label": "Maid", + "value": "4504" + }, + { + "label": "Militaryco", + "value": "4534" + }, + { + "label": "Mythical", + "value": "4638" + }, + { + "label": "MultipleRe", + "value": "4643" + }, + { + "label": "Millionair", + "value": "4683" + }, + { + "label": "Mutan", + "value": "4694" + }, + { + "label": "MindBreak", + "value": "4717" + }, + { + "label": "MaleProtag", + "value": "4721" + }, + { + "label": "machine", + "value": "4747" + }, + { + "label": "Martialart", + "value": "4765" + }, + { + "label": "Mangaka", + "value": "4817" + }, + { + "label": "Mysterious", + "value": "4828" + }, + { + "label": "Naruto", + "value": "653" + }, + { + "label": "nba", + "value": "1342" + }, + { + "label": "Nodiscipli", + "value": "4231" + }, + { + "label": "Nationalis", + "value": "451" + }, + { + "label": "女神", + "value": "2818" + }, + { + "label": "NoCP", + "value": "4300" + }, + { + "label": "NaiveProta", + "value": "3718" + }, + { + "label": "Npc", + "value": "798" + }, + { + "label": "Nobles", + "value": "38" + }, + { + "label": "noheroine", + "value": "4242" + }, + { + "label": "Ninjas", + "value": "982" + }, + { + "label": "Nogoldenfi", + "value": "4434" + }, + { + "label": "Nonhuman", + "value": "415" + }, + { + "label": "Non-System", + "value": "1262" + }, + { + "label": "Noromance", + "value": "662" + }, + { + "label": "Netori", + "value": "609" + }, + { + "label": "Necromance", + "value": "734" + }, + { + "label": "NationalGa", + "value": "4454" + }, + { + "label": "Netorare", + "value": "3820" + }, + { + "label": "No-Harem", + "value": "2879" + }, + { + "label": "Near-Death", + "value": "3843" + }, + { + "label": "Nakaji", + "value": "3494" + }, + { + "label": "Nocheats", + "value": "804" + }, + { + "label": "Necromancy", + "value": "1325" + }, + { + "label": "NoHarem", + "value": "4028" + }, + { + "label": "Nurses", + "value": "4723" + }, + { + "label": "Non-humanP", + "value": "4076" + }, + { + "label": "NoSystem", + "value": "4075" + }, + { + "label": "Navy", + "value": "4150" + }, + { + "label": "Netred", + "value": "4439" + }, + { + "label": "Nowsay", + "value": "4533" + }, + { + "label": "Nightmare", + "value": "4598" + }, + { + "label": "Noble", + "value": "4700" + }, + { + "label": "Nerd", + "value": "3895" + }, + { + "label": "Narcissist", + "value": "3696" + }, + { + "label": "Non-humano", + "value": "3782" + }, + { + "label": "Need", + "value": "4510" + }, + { + "label": "Nightmares", + "value": "4670" + }, + { + "label": "Novel", + "value": "4768" + }, + { + "label": "Neet", + "value": "4806" + }, + { + "label": "Non-linear", + "value": "4845" + }, + { + "label": "Overhead", + "value": "2883" + }, + { + "label": "OnePiece", + "value": "942" + }, + { + "label": "Obsession", + "value": "1311" + }, + { + "label": "Overpowere", + "value": "103" + }, + { + "label": "openingflo", + "value": "4271" + }, + { + "label": "Otaku", + "value": "553" + }, + { + "label": "OrientalFa", + "value": "4443" + }, + { + "label": "OuterSpace", + "value": "3783" + }, + { + "label": "openflow", + "value": "4240" + }, + { + "label": "Onlinegame", + "value": "4252" + }, + { + "label": "overbearin", + "value": "3273" + }, + { + "label": "Open", + "value": "3165" + }, + { + "label": "Orphans", + "value": "3723" + }, + { + "label": "OPMC", + "value": "3942" + }, + { + "label": "originalco", + "value": "4366" + }, + { + "label": "Office", + "value": "3294" + }, + { + "label": "onlinegame", + "value": "4257" + }, + { + "label": "OlderLoveI", + "value": "4588" + }, + { + "label": "Orcs", + "value": "4069" + }, + { + "label": "ObsessiveL", + "value": "4668" + }, + { + "label": "OrganizedC", + "value": "3950" + }, + { + "label": "Onlinegami", + "value": "535" + }, + { + "label": "Onocat", + "value": "4506" + }, + { + "label": "OfficeRoma", + "value": "4621" + }, + { + "label": "Omegaverse", + "value": "4649" + }, + { + "label": "overpower", + "value": "4759" + }, + { + "label": "OPProtagon", + "value": "4102" + }, + { + "label": "Openupwast", + "value": "4386" + }, + { + "label": "Orphan", + "value": "4050" + }, + { + "label": "Overpowere", + "value": "3884" + }, + { + "label": "OnlineRoma", + "value": "3652" + }, + { + "label": "OnlineGame", + "value": "4172" + }, + { + "label": "Ory", + "value": "4154" + }, + { + "label": "Overpowere", + "value": "3641" + }, + { + "label": "Orgy", + "value": "4155" + }, + { + "label": "Onepunch", + "value": "3961" + }, + { + "label": "OldMovieso", + "value": "4472" + }, + { + "label": "Originalfi", + "value": "4482" + }, + { + "label": "Ordinary", + "value": "4572" + }, + { + "label": "On-HookSys", + "value": "4697" + }, + { + "label": "OpFemalePr", + "value": "4742" + }, + { + "label": "Officialwo", + "value": "4767" + }, + { + "label": "Onlinegame", + "value": "4770" + }, + { + "label": "President", + "value": "2932" + }, + { + "label": "pet", + "value": "2827" + }, + { + "label": "Plane", + "value": "3214" + }, + { + "label": "Pingbuqing", + "value": "2793" + }, + { + "label": "Pirate", + "value": "3056" + }, + { + "label": "PoortoRich", + "value": "198" + }, + { + "label": "Pokemon", + "value": "713" + }, + { + "label": "Possession", + "value": "1312" + }, + { + "label": "Pets", + "value": "276" + }, + { + "label": "positiveen", + "value": "4298" + }, + { + "label": "Paranoid", + "value": "3025" + }, + { + "label": "petarticle", + "value": "4206" + }, + { + "label": "practice", + "value": "3268" + }, + { + "label": "ParallelWo", + "value": "3790" + }, + { + "label": "Pregnancy", + "value": "39" + }, + { + "label": "PureLove", + "value": "1315" + }, + { + "label": "Pirates", + "value": "629" + }, + { + "label": "Player", + "value": "3447" + }, + { + "label": "Pros", + "value": "3422" + }, + { + "label": "Proud", + "value": "3423" + }, + { + "label": "Polygamy", + "value": "86" + }, + { + "label": "Politics", + "value": "3948" + }, + { + "label": "physicaled", + "value": "4433" + }, + { + "label": "policyflow", + "value": "4232" + }, + { + "label": "Police", + "value": "76" + }, + { + "label": "protectsho", + "value": "4379" + }, + { + "label": "Post-apoca", + "value": "4627" + }, + { + "label": "PowerCoupl", + "value": "3690" + }, + { + "label": "Possessive", + "value": "169" + }, + { + "label": "profession", + "value": "4281" + }, + { + "label": "Powerfulco", + "value": "171" + }, + { + "label": "Prince", + "value": "3426" + }, + { + "label": "PreviousLi", + "value": "4053" + }, + { + "label": "PrinceofTe", + "value": "1296" + }, + { + "label": "Possessive", + "value": "4586" + }, + { + "label": "Poisons", + "value": "191" + }, + { + "label": "Princess", + "value": "364" + }, + { + "label": "practicefl", + "value": "4253" + }, + { + "label": "PastandPre", + "value": "4364" + }, + { + "label": "Policemen", + "value": "3331" + }, + { + "label": "PervertedP", + "value": "3769" + }, + { + "label": "PoorProtag", + "value": "3896" + }, + { + "label": "Psychologi", + "value": "870" + }, + { + "label": "PastPlaysa", + "value": "3673" + }, + { + "label": "Powerhouse", + "value": "3544" + }, + { + "label": "ParallelWo", + "value": "3864" + }, + { + "label": "PragmaticP", + "value": "4185" + }, + { + "label": "Positive", + "value": "23" + }, + { + "label": "puppy", + "value": "3490" + }, + { + "label": "Personalit", + "value": "4658" + }, + { + "label": "PillConcoc", + "value": "4698" + }, + { + "label": "Psychopath", + "value": "3906" + }, + { + "label": "Pilots", + "value": "3850" + }, + { + "label": "PlayfulPro", + "value": "3826" + }, + { + "label": "Poisonoust", + "value": "4412" + }, + { + "label": "Parody", + "value": "4779" + }, + { + "label": "PoliteProt", + "value": "4051" + }, + { + "label": "Prostitute", + "value": "3890" + }, + { + "label": "Protagonis", + "value": "1268" + }, + { + "label": "Playboys", + "value": "3823" + }, + { + "label": "PowerStrug", + "value": "3761" + }, + { + "label": "Popularsci", + "value": "4448" + }, + { + "label": "Presidentw", + "value": "4473" + }, + { + "label": "Prophecies", + "value": "4616" + }, + { + "label": "Pharmacist", + "value": "4821" + }, + { + "label": "PastTrauma", + "value": "3903" + }, + { + "label": "Prison", + "value": "4156" + }, + { + "label": "PopularLov", + "value": "4622" + }, + { + "label": "ProactiveP", + "value": "4708" + }, + { + "label": "PsychicPow", + "value": "4725" + }, + { + "label": "Poetry", + "value": "4796" + }, + { + "label": "Puppeteers", + "value": "3977" + }, + { + "label": "Priest", + "value": "4267" + }, + { + "label": "Physician", + "value": "4413" + }, + { + "label": "Portablesp", + "value": "4462" + }, + { + "label": "Playpigeat", + "value": "4527" + }, + { + "label": "personalco", + "value": "4549" + }, + { + "label": "ParentComp", + "value": "4636" + }, + { + "label": "Philosophi", + "value": "4786" + }, + { + "label": "PillBasedC", + "value": "4811" + }, + { + "label": "Psychology", + "value": "4072" + }, + { + "label": "PoliticalI", + "value": "3956" + }, + { + "label": "Psychic", + "value": "3974" + }, + { + "label": "Protagonis", + "value": "3954" + }, + { + "label": "Protagonis", + "value": "3789" + }, + { + "label": "Paladin", + "value": "4045" + }, + { + "label": "PowersTran", + "value": "3973" + }, + { + "label": "Perfectwor", + "value": "4190" + }, + { + "label": "Pretendtob", + "value": "4208" + }, + { + "label": "poisondoct", + "value": "4558" + }, + { + "label": "Pony", + "value": "4564" + }, + { + "label": "Protagonis", + "value": "4607" + }, + { + "label": "Polyandry", + "value": "4631" + }, + { + "label": "Playboymal", + "value": "4675" + }, + { + "label": "PreviousLi", + "value": "4685" + }, + { + "label": "PamperingR", + "value": "4689" + }, + { + "label": "PastPlaysa", + "value": "4712" + }, + { + "label": "Programmer", + "value": "4715" + }, + { + "label": "PlaneWars", + "value": "4728" + }, + { + "label": "Poor", + "value": "4743" + }, + { + "label": "PillConcot", + "value": "4753" + }, + { + "label": "Paizuri", + "value": "4783" + }, + { + "label": "Parasites", + "value": "4801" + }, + { + "label": "Phoenixes", + "value": "4819" + }, + { + "label": "Part-TimeJ", + "value": "4824" + }, + { + "label": "Priests", + "value": "4835" + }, + { + "label": "PretendLov", + "value": "4844" + }, + { + "label": "Qishen", + "value": "2759" + }, + { + "label": "qidian", + "value": "4570" + }, + { + "label": "QuickTrans", + "value": "3875" + }, + { + "label": "QinHan", + "value": "4385" + }, + { + "label": "qimao", + "value": "4579" + }, + { + "label": "Question&a", + "value": "4141" + }, + { + "label": "Queen", + "value": "4562" + }, + { + "label": "QuirkyChar", + "value": "4816" + }, + { + "label": "Rebirth", + "value": "224" + }, + { + "label": "Relaxed", + "value": "2817" + }, + { + "label": "Reunion", + "value": "2694" + }, + { + "label": "Reincarnat", + "value": "14" + }, + { + "label": "Romance", + "value": "8" + }, + { + "label": "Revenge", + "value": "40" + }, + { + "label": "R18", + "value": "856" + }, + { + "label": "Raiders", + "value": "3229" + }, + { + "label": "rob", + "value": "2861" + }, + { + "label": "ReikiRecov", + "value": "4249" + }, + { + "label": "Racism", + "value": "442" + }, + { + "label": "RebirthedP", + "value": "1264" + }, + { + "label": "RomanticSu", + "value": "3674" + }, + { + "label": "Regret", + "value": "1314" + }, + { + "label": "Returnofth", + "value": "4213" + }, + { + "label": "RuthlessPr", + "value": "3727" + }, + { + "label": "Regression", + "value": "1316" + }, + { + "label": "Reverse", + "value": "3168" + }, + { + "label": "rejoice", + "value": "3215" + }, + { + "label": "R-18", + "value": "906" + }, + { + "label": "rainyseaso", + "value": "4211" + }, + { + "label": "Royal", + "value": "3351" + }, + { + "label": "RoyalBeast", + "value": "4287" + }, + { + "label": "Royalty", + "value": "41" + }, + { + "label": "Rape", + "value": "149" + }, + { + "label": "reincarnat", + "value": "3227" + }, + { + "label": "redpacketf", + "value": "4370" + }, + { + "label": "rebirthhou", + "value": "4466" + }, + { + "label": "Royalfamil", + "value": "398" + }, + { + "label": "Refiner", + "value": "3439" + }, + { + "label": "Rollover", + "value": "3415" + }, + { + "label": "RighteousP", + "value": "3665" + }, + { + "label": "Rarebloodl", + "value": "210" + }, + { + "label": "Religions", + "value": "4119" + }, + { + "label": "Resurrecti", + "value": "4047" + }, + { + "label": "RomanceFan", + "value": "1319" + }, + { + "label": "rebirthrev", + "value": "4526" + }, + { + "label": "ReverseHar", + "value": "4592" + }, + { + "label": "ReverseRap", + "value": "3913" + }, + { + "label": "R-15", + "value": "3796" + }, + { + "label": "RaceChange", + "value": "3827" + }, + { + "label": "readstream", + "value": "4328" + }, + { + "label": "richandbea", + "value": "4357" + }, + { + "label": "randomstre", + "value": "4426" + }, + { + "label": "Richdaught", + "value": "157" + }, + { + "label": "Reborn", + "value": "215" + }, + { + "label": "rawstream", + "value": "4390" + }, + { + "label": "rebirthwit", + "value": "4532" + }, + { + "label": "RapeVictim", + "value": "4032" + }, + { + "label": "RichProtag", + "value": "3897" + }, + { + "label": "Rpe", + "value": "4031" + }, + { + "label": "Rebellion", + "value": "3774" + }, + { + "label": "RimuruTemp", + "value": "4103" + }, + { + "label": "Reversal", + "value": "4226" + }, + { + "label": "Racingcar", + "value": "4360" + }, + { + "label": "richpeople", + "value": "4382" + }, + { + "label": "rural", + "value": "4519" + }, + { + "label": "reversewea", + "value": "4546" + }, + { + "label": "ReligousOr", + "value": "4046" + }, + { + "label": "Role-Playi", + "value": "3933" + }, + { + "label": "RomanticSu", + "value": "4038" + }, + { + "label": "RapeVictim", + "value": "3857" + }, + { + "label": "Reincarnat", + "value": "3646" + }, + { + "label": "Reincarnat", + "value": "3988" + }, + { + "label": "Returningf", + "value": "3766" + }, + { + "label": "Reincarnat", + "value": "3784" + }, + { + "label": "日常", + "value": "4333" + }, + { + "label": "RedHouse", + "value": "4377" + }, + { + "label": "RuneMaster", + "value": "4523" + }, + { + "label": "Richestman", + "value": "4538" + }, + { + "label": "Return", + "value": "4567" + }, + { + "label": "reasoning", + "value": "4575" + }, + { + "label": "RanchFarmi", + "value": "4764" + }, + { + "label": "Rivalry", + "value": "4802" + }, + { + "label": "Reporters", + "value": "4807" + }, + { + "label": "Reversible", + "value": "4815" + }, + { + "label": "Roommates", + "value": "4827" + }, + { + "label": "System", + "value": "24" + }, + { + "label": "strong", + "value": "2744" + }, + { + "label": "systemflow", + "value": "4223" + }, + { + "label": "Shuangwen", + "value": "2705" + }, + { + "label": "Supernatur", + "value": "318" + }, + { + "label": "Slap", + "value": "2728" + }, + { + "label": "Sports", + "value": "685" + }, + { + "label": "Shenhao", + "value": "3062" + }, + { + "label": "sweetpet", + "value": "4196" + }, + { + "label": "streamofhe", + "value": "4280" + }, + { + "label": "Start", + "value": "3065" + }, + { + "label": "strongfema", + "value": "4215" + }, + { + "label": "Strategy", + "value": "3251" + }, + { + "label": "Student", + "value": "2953" + }, + { + "label": "Space-time", + "value": "4321" + }, + { + "label": "SpecialAbi", + "value": "3812" + }, + { + "label": "Signin", + "value": "833" + }, + { + "label": "SingleHero", + "value": "4086" + }, + { + "label": "subject", + "value": "2889" + }, + { + "label": "Sanguanzhe", + "value": "2957" + }, + { + "label": "Summoningf", + "value": "4247" + }, + { + "label": "Self-disci", + "value": "3022" + }, + { + "label": "Superpower", + "value": "26" + }, + { + "label": "Survive", + "value": "3276" + }, + { + "label": "scumbag", + "value": "3208" + }, + { + "label": "son-in-law", + "value": "3342" + }, + { + "label": "Sliceoflif", + "value": "248" + }, + { + "label": "Showbiz", + "value": "68" + }, + { + "label": "Sign-InChe", + "value": "1266" + }, + { + "label": "Survival", + "value": "52" + }, + { + "label": "StrongtoSt", + "value": "108" + }, + { + "label": "Shurachang", + "value": "2970" + }, + { + "label": "stranger", + "value": "3224" + }, + { + "label": "Schoolflow", + "value": "4440" + }, + { + "label": "sick", + "value": "3026" + }, + { + "label": "Shuangjie", + "value": "3231" + }, + { + "label": "SD", + "value": "2716" + }, + { + "label": "Sciencefic", + "value": "4127" + }, + { + "label": "StraightAs", + "value": "4216" + }, + { + "label": "SuperTechn", + "value": "1275" + }, + { + "label": "SecondChan", + "value": "4124" + }, + { + "label": "Scientists", + "value": "3818" + }, + { + "label": "SystemAdmi", + "value": "3668" + }, + { + "label": "straightma", + "value": "4274" + }, + { + "label": "Suspense", + "value": "3366" + }, + { + "label": "Streamwith", + "value": "4291" + }, + { + "label": "SwordWield", + "value": "3724" + }, + { + "label": "SwordAndMa", + "value": "508" + }, + { + "label": "SummoningM", + "value": "3817" + }, + { + "label": "supplier", + "value": "3191" + }, + { + "label": "Specialfor", + "value": "4220" + }, + { + "label": "Sweetlove", + "value": "66" + }, + { + "label": "SurvivalGa", + "value": "516" + }, + { + "label": "SlowGrowth", + "value": "3715" + }, + { + "label": "Solitarype", + "value": "4342" + }, + { + "label": "suspensebr", + "value": "4453" + }, + { + "label": "Strategist", + "value": "4628" + }, + { + "label": "SlowRomanc", + "value": "3762" + }, + { + "label": "Saltyandsw", + "value": "4400" + }, + { + "label": "StrongProt", + "value": "1147" + }, + { + "label": "strongstre", + "value": "4323" + }, + { + "label": "ShamelessP", + "value": "3811" + }, + { + "label": "Strongback", + "value": "855" + }, + { + "label": "神话", + "value": "3270" + }, + { + "label": "SurvivalDo", + "value": "4515" + }, + { + "label": "SuddenWeal", + "value": "4040" + }, + { + "label": "SuperA", + "value": "4346" + }, + { + "label": "swordrepai", + "value": "4352" + }, + { + "label": "Smut", + "value": "858" + }, + { + "label": "spoiler", + "value": "3324" + }, + { + "label": "Strongfrom", + "value": "3821" + }, + { + "label": "Shijia", + "value": "3269" + }, + { + "label": "softricefl", + "value": "4295" + }, + { + "label": "SuiandTang", + "value": "4359" + }, + { + "label": "SentientSk", + "value": "1277" + }, + { + "label": "struggle", + "value": "3345" + }, + { + "label": "sweettext", + "value": "4458" + }, + { + "label": "Sci-Fi", + "value": "83" + }, + { + "label": "Steampunk", + "value": "2972" + }, + { + "label": "Securitygu", + "value": "4218" + }, + { + "label": "Suspensefu", + "value": "4236" + }, + { + "label": "showdownfl", + "value": "4388" + }, + { + "label": "seeyouagai", + "value": "4397" + }, + { + "label": "Singers", + "value": "4735" + }, + { + "label": "SecretIden", + "value": "4054" + }, + { + "label": "Summons", + "value": "420" + }, + { + "label": "Stand-alon", + "value": "3551" + }, + { + "label": "SecretOrga", + "value": "4055" + }, + { + "label": "SoulPower", + "value": "3908" + }, + { + "label": "Salvation", + "value": "1313" + }, + { + "label": "SecretOrga", + "value": "3775" + }, + { + "label": "StrongLove", + "value": "4122" + }, + { + "label": "SexSlaves", + "value": "3929" + }, + { + "label": "SmartCoupl", + "value": "3945" + }, + { + "label": "substitute", + "value": "3500" + }, + { + "label": "SexualAbus", + "value": "3859" + }, + { + "label": "self-impro", + "value": "3258" + }, + { + "label": "SectDevelo", + "value": "3881" + }, + { + "label": "StoreOwner", + "value": "4011" + }, + { + "label": "Spoil", + "value": "3464" + }, + { + "label": "Shoujo-AiS", + "value": "3819" + }, + { + "label": "Spaceship", + "value": "3785" + }, + { + "label": "SemeProtag", + "value": "3926" + }, + { + "label": "Sweet", + "value": "421" + }, + { + "label": "SkillAssim", + "value": "3805" + }, + { + "label": "Saltedfish", + "value": "4316" + }, + { + "label": "Slaves", + "value": "4602" + }, + { + "label": "Strongfema", + "value": "54" + }, + { + "label": "宋朝", + "value": "3454" + }, + { + "label": "Shounen-Ai", + "value": "3927" + }, + { + "label": "Spoiling", + "value": "160" + }, + { + "label": "SuddenStre", + "value": "3883" + }, + { + "label": "Spirits", + "value": "4004" + }, + { + "label": "SkillCreat", + "value": "3806" + }, + { + "label": "SchoolLife", + "value": "4117" + }, + { + "label": "SelfishPro", + "value": "3719" + }, + { + "label": "SlaveProta", + "value": "3682" + }, + { + "label": "SisterYu", + "value": "4429" + }, + { + "label": "schoolgras", + "value": "4497" + }, + { + "label": "sweetpetar", + "value": "4529" + }, + { + "label": "Soldiers", + "value": "4625" + }, + { + "label": "SicklyChar", + "value": "4650" + }, + { + "label": "Scary", + "value": "635" + }, + { + "label": "Shapeshift", + "value": "645" + }, + { + "label": "SigninChec", + "value": "1292" + }, + { + "label": "Secrets", + "value": "3992" + }, + { + "label": "SpatialMan", + "value": "3642" + }, + { + "label": "Saints", + "value": "4097" + }, + { + "label": "SerialKill", + "value": "4129" + }, + { + "label": "StrategicB", + "value": "3636" + }, + { + "label": "Startabusi", + "value": "4365" + }, + { + "label": "shortforpa", + "value": "4372" + }, + { + "label": "SupremeStr", + "value": "4380" + }, + { + "label": "Scifi", + "value": "745" + }, + { + "label": "Schemesand", + "value": "3991" + }, + { + "label": "SisterComp", + "value": "4161" + }, + { + "label": "SealGod", + "value": "4293" + }, + { + "label": "Suspensefl", + "value": "4384" + }, + { + "label": "sillywhite", + "value": "4427" + }, + { + "label": "Sisterandb", + "value": "4484" + }, + { + "label": "Soccer", + "value": "4617" + }, + { + "label": "SealedPowe", + "value": "4618" + }, + { + "label": "SavingtheW", + "value": "4624" + }, + { + "label": "Suicides", + "value": "4671" + }, + { + "label": "Souls", + "value": "3909" + }, + { + "label": "Sharp-tong", + "value": "4039" + }, + { + "label": "SCP", + "value": "4026" + }, + { + "label": "SmartMC", + "value": "3951" + }, + { + "label": "SaintSeiya", + "value": "3976" + }, + { + "label": "Strength-b", + "value": "3692" + }, + { + "label": "Shushan", + "value": "4279" + }, + { + "label": "Survivalch", + "value": "4391" + }, + { + "label": "spookygame", + "value": "4508" + }, + { + "label": "SkillBooks", + "value": "4596" + }, + { + "label": "SinglePare", + "value": "4604" + }, + { + "label": "SlaveHarem", + "value": "4641" + }, + { + "label": "Succubus", + "value": "4642" + }, + { + "label": "Siblings", + "value": "4659" + }, + { + "label": "smartprota", + "value": "4674" + }, + { + "label": "SexualCult", + "value": "4677" + }, + { + "label": "SecretiveP", + "value": "4679" + }, + { + "label": "SpiritUser", + "value": "4781" + }, + { + "label": "ShyCharact", + "value": "3925" + }, + { + "label": "SadisticCh", + "value": "3907" + }, + { + "label": "Sisters", + "value": "3914" + }, + { + "label": "Scriptwrit", + "value": "3898" + }, + { + "label": "SuperPower", + "value": "3899" + }, + { + "label": "SuperSemin", + "value": "3892" + }, + { + "label": "StrongSubo", + "value": "3891" + }, + { + "label": "SectMaster", + "value": "3882" + }, + { + "label": "SlaveSyste", + "value": "3858" + }, + { + "label": "Servants", + "value": "3824" + }, + { + "label": "suicidalpr", + "value": "4166" + }, + { + "label": "SaikiK.", + "value": "4057" + }, + { + "label": "Sign-in", + "value": "4191" + }, + { + "label": "Student-Te", + "value": "4114" + }, + { + "label": "Shounen", + "value": "4118" + }, + { + "label": "Stand", + "value": "4171" + }, + { + "label": "Simulator", + "value": "4168" + }, + { + "label": "SiblingsNo", + "value": "4160" + }, + { + "label": "SeeingThin", + "value": "4094" + }, + { + "label": "Saint", + "value": "3691" + }, + { + "label": "Shotacon", + "value": "4101" + }, + { + "label": "SaikiK", + "value": "4140" + }, + { + "label": "Sea", + "value": "4081" + }, + { + "label": "SchemesAnd", + "value": "3666" + }, + { + "label": "Sportscomp", + "value": "4258" + }, + { + "label": "Space", + "value": "4335" + }, + { + "label": "Summoner", + "value": "4339" + }, + { + "label": "SerpentKin", + "value": "4418" + }, + { + "label": "slackerstu", + "value": "4488" + }, + { + "label": "Strongmena", + "value": "4489" + }, + { + "label": "Senior", + "value": "4543" + }, + { + "label": "Smalldogs", + "value": "4561" + }, + { + "label": "Spaceopera", + "value": "4576" + }, + { + "label": "SelflessPr", + "value": "4609" + }, + { + "label": "Sects", + "value": "4660" + }, + { + "label": "Stalkers", + "value": "4665" + }, + { + "label": "Slave", + "value": "4678" + }, + { + "label": "Schemes", + "value": "4684" + }, + { + "label": "Siscon", + "value": "4704" + }, + { + "label": "Simulation", + "value": "4710" + }, + { + "label": "Sibling&am", + "value": "4714" + }, + { + "label": "StraightSe", + "value": "4729" + }, + { + "label": "Stubborn", + "value": "4736" + }, + { + "label": "Seduction", + "value": "4739" + }, + { + "label": "strongwoma", + "value": "4740" + }, + { + "label": "skycity", + "value": "4748" + }, + { + "label": "Sectbuildi", + "value": "4758" + }, + { + "label": "Sciencefic", + "value": "4760" + }, + { + "label": "Seinen", + "value": "4771" + }, + { + "label": "StockholmS", + "value": "4785" + }, + { + "label": "Sibling&am", + "value": "4804" + }, + { + "label": "Sentimenta", + "value": "4813" + }, + { + "label": "Shota", + "value": "4820" + }, + { + "label": "SummonedHe", + "value": "4822" + }, + { + "label": "Spies", + "value": "4842" + }, + { + "label": "Transmigra", + "value": "15" + }, + { + "label": "Two-dimens", + "value": "3054" + }, + { + "label": "Tutor", + "value": "2720" + }, + { + "label": "TimeTravel", + "value": "3637" + }, + { + "label": "Technology", + "value": "3211" + }, + { + "label": "Tomb", + "value": "2719" + }, + { + "label": "ThreeKingd", + "value": "4292" + }, + { + "label": "Tenderness", + "value": "2791" + }, + { + "label": "timegate", + "value": "4272" + }, + { + "label": "thunderbol", + "value": "2755" + }, + { + "label": "teacher", + "value": "3213" + }, + { + "label": "Takeaway", + "value": "3334" + }, + { + "label": "Talents", + "value": "1265" + }, + { + "label": "TangDynast", + "value": "4356" + }, + { + "label": "Tragedy", + "value": "507" + }, + { + "label": "technology", + "value": "4250" + }, + { + "label": "TimeSkip", + "value": "3694" + }, + { + "label": "Technologi", + "value": "3771" + }, + { + "label": "TradeWar", + "value": "4275" + }, + { + "label": "thescienti", + "value": "4324" + }, + { + "label": "two-waycru", + "value": "4261" + }, + { + "label": "Thestronga", + "value": "28" + }, + { + "label": "Tsundere", + "value": "139" + }, + { + "label": "TheFourthC", + "value": "4244" + }, + { + "label": "TianTingwe", + "value": "4288" + }, + { + "label": "Troubledti", + "value": "4318" + }, + { + "label": "Taoistprie", + "value": "4350" + }, + { + "label": "theInterne", + "value": "4503" + }, + { + "label": "Thriller", + "value": "4666" + }, + { + "label": "trickery", + "value": "3292" + }, + { + "label": "TreasureCh", + "value": "4371" + }, + { + "label": "TragicPast", + "value": "3866" + }, + { + "label": "trackandfi", + "value": "4330" + }, + { + "label": "thrillersu", + "value": "4468" + }, + { + "label": "TimeManipu", + "value": "4615" + }, + { + "label": "Twins", + "value": "4718" + }, + { + "label": "Thieves", + "value": "4809" + }, + { + "label": "Teamwork", + "value": "4599" + }, + { + "label": "Transplant", + "value": "4750" + }, + { + "label": "takehome", + "value": "4311" + }, + { + "label": "Teachers", + "value": "3978" + }, + { + "label": "tennis", + "value": "2995" + }, + { + "label": "Traditiona", + "value": "4451" + }, + { + "label": "Toughgirl", + "value": "4415" + }, + { + "label": "Terracenea", + "value": "4435" + }, + { + "label": "TribalSoci", + "value": "4707" + }, + { + "label": "Teen", + "value": "857" + }, + { + "label": "TerritoryC", + "value": "1276" + }, + { + "label": "Trueandfal", + "value": "4507" + }, + { + "label": "TwistedPer", + "value": "4672" + }, + { + "label": "Transmigra", + "value": "4014" + }, + { + "label": "Technology", + "value": "3970" + }, + { + "label": "TowerDefen", + "value": "3998" + }, + { + "label": "Threesome", + "value": "3807" + }, + { + "label": "Transforma", + "value": "4138" + }, + { + "label": "Type-moon", + "value": "4105" + }, + { + "label": "Tensura", + "value": "4104" + }, + { + "label": "Talentedgi", + "value": "4428" + }, + { + "label": "Travelthro", + "value": "4551" + }, + { + "label": "TimeLoop", + "value": "4645" + }, + { + "label": "Torture", + "value": "4663" + }, + { + "label": "Toriko", + "value": "4773" + }, + { + "label": "team", + "value": "4036" + }, + { + "label": "Transporte", + "value": "3860" + }, + { + "label": "Transporte", + "value": "3840" + }, + { + "label": "Titans", + "value": "3776" + }, + { + "label": "Transmigra", + "value": "3979" + }, + { + "label": "Transporte", + "value": "4187" + }, + { + "label": "Transmigat", + "value": "4163" + }, + { + "label": "Traveling", + "value": "4082" + }, + { + "label": "TokyoGhoul", + "value": "4148" + }, + { + "label": "Thief", + "value": "4317" + }, + { + "label": "technology", + "value": "4334" + }, + { + "label": "Technology", + "value": "4363" + }, + { + "label": "Topstream", + "value": "4420" + }, + { + "label": "Threerelig", + "value": "4469" + }, + { + "label": "Tianshi", + "value": "4479" + }, + { + "label": "Transform", + "value": "4552" + }, + { + "label": "TS", + "value": "4569" + }, + { + "label": "Trap", + "value": "4600" + }, + { + "label": "TomboyishF", + "value": "4603" + }, + { + "label": "Transmigra", + "value": "4661" + }, + { + "label": "Talent", + "value": "4737" + }, + { + "label": "travel", + "value": "4749" + }, + { + "label": "TimidProta", + "value": "4825" + }, + { + "label": "Terrorists", + "value": "4829" + }, + { + "label": "TimeParado", + "value": "4832" + }, + { + "label": "Urbanbrain", + "value": "4197" + }, + { + "label": "upgrade", + "value": "3170" + }, + { + "label": "upgradeflo", + "value": "4282" + }, + { + "label": "Urban", + "value": "104" + }, + { + "label": "UnlimitedF", + "value": "4751" + }, + { + "label": "urbanfarmi", + "value": "4446" + }, + { + "label": "UrbanCulti", + "value": "4199" + }, + { + "label": "urbanimmor", + "value": "4306" + }, + { + "label": "Ultrafan", + "value": "4461" + }, + { + "label": "UrbanAbili", + "value": "4442" + }, + { + "label": "Urbanlife", + "value": "4207" + }, + { + "label": "UncleJiu", + "value": "4513" + }, + { + "label": "Undead", + "value": "776" + }, + { + "label": "urbanroman", + "value": "4539" + }, + { + "label": "Uncle", + "value": "3586" + }, + { + "label": "Undercover", + "value": "3226" + }, + { + "label": "UglytoBeau", + "value": "4632" + }, + { + "label": "Unprincipl", + "value": "751" + }, + { + "label": "Unknown", + "value": "1789" + }, + { + "label": "urbanyouth", + "value": "4520" + }, + { + "label": "UnluckyPro", + "value": "4795" + }, + { + "label": "UglyProtag", + "value": "3740" + }, + { + "label": "Ugly", + "value": "4353" + }, + { + "label": "urbanworkp", + "value": "4531" + }, + { + "label": "UniqueCult", + "value": "4583" + }, + { + "label": "Unconditio", + "value": "4610" + }, + { + "label": "Unreliable", + "value": "4070" + }, + { + "label": "Urbanfanta", + "value": "4478" + }, + { + "label": "Underestim", + "value": "4619" + }, + { + "label": "Upgradeand", + "value": "4722" + }, + { + "label": "UrbanRoman", + "value": "4769" + }, + { + "label": "Villain", + "value": "207" + }, + { + "label": "vest", + "value": "2867" + }, + { + "label": "Videogame", + "value": "562" + }, + { + "label": "VirtualRea", + "value": "79" + }, + { + "label": "Vampire", + "value": "115" + }, + { + "label": "Variety", + "value": "3430" + }, + { + "label": "Vampires", + "value": "5" + }, + { + "label": "Voice", + "value": "3435" + }, + { + "label": "Villainpro", + "value": "649" + }, + { + "label": "VinegarKin", + "value": "4308" + }, + { + "label": "videostrea", + "value": "4331" + }, + { + "label": "Villainess", + "value": "4793" + }, + { + "label": "VoiceActor", + "value": "4814" + }, + { + "label": "wealthyfam", + "value": "4203" + }, + { + "label": "WeaktoStro", + "value": "2" + }, + { + "label": "wealthypre", + "value": "4200" + }, + { + "label": "Wealthy", + "value": "2931" + }, + { + "label": "Whimsical", + "value": "2706" + }, + { + "label": "Workplace", + "value": "2769" + }, + { + "label": "Wisdom", + "value": "2922" + }, + { + "label": "wearbook", + "value": "4303" + }, + { + "label": "WorldHoppi", + "value": "426" + }, + { + "label": "Wizards", + "value": "136" + }, + { + "label": "Warm", + "value": "3176" + }, + { + "label": "Wars", + "value": "19" + }, + { + "label": "WorldTrave", + "value": "3643" + }, + { + "label": "WealthyCha", + "value": "3867" + }, + { + "label": "WestwardJo", + "value": "4296" + }, + { + "label": "warrior", + "value": "3247" + }, + { + "label": "War", + "value": "601" + }, + { + "label": "王者荣耀", + "value": "4256" + }, + { + "label": "Writers", + "value": "3854" + }, + { + "label": "Werewolf", + "value": "164" + }, + { + "label": "Writer", + "value": "3328" + }, + { + "label": "wizardstre", + "value": "4266" + }, + { + "label": "WebNovel", + "value": "4577" + }, + { + "label": "Wastewoodf", + "value": "4383" + }, + { + "label": "WesternRom", + "value": "4471" + }, + { + "label": "Westernfan", + "value": "4355" + }, + { + "label": "WeakProtag", + "value": "3716" + }, + { + "label": "Witches", + "value": "3845" + }, + { + "label": "White", + "value": "3296" + }, + { + "label": "whitelotus", + "value": "4309" + }, + { + "label": "Whitemoonl", + "value": "4409" + }, + { + "label": "WaterMargi", + "value": "4319" + }, + { + "label": "Warmpettex", + "value": "4475" + }, + { + "label": "WarsWeakto", + "value": "3868" + }, + { + "label": "WorldofWar", + "value": "4139" + }, + { + "label": "Werebeasts", + "value": "4095" + }, + { + "label": "whitecolla", + "value": "4457" + }, + { + "label": "wearback", + "value": "4485" + }, + { + "label": "Warmman", + "value": "4423" + }, + { + "label": "Wuxia", + "value": "4633" + }, + { + "label": "Wizard", + "value": "3931" + }, + { + "label": "workplaceb", + "value": "4474" + }, + { + "label": "WOW", + "value": "4176" + }, + { + "label": "Werewolves", + "value": "3993" + }, + { + "label": "Warship", + "value": "4083" + }, + { + "label": "Warcraft", + "value": "4481" + }, + { + "label": "Wasteland", + "value": "4656" + }, + { + "label": "WW2", + "value": "4726" + }, + { + "label": "Wishes", + "value": "4840" + }, + { + "label": "Xijing", + "value": "3234" + }, + { + "label": "Xiuxian", + "value": "3271" + }, + { + "label": "Xianxia", + "value": "506" + }, + { + "label": "Xiaosan", + "value": "2857" + }, + { + "label": "Xuanhuan", + "value": "882" + }, + { + "label": "Xianzun", + "value": "3478" + }, + { + "label": "Xiuwaihuih", + "value": "4411" + }, + { + "label": "Xianjun", + "value": "4557" + }, + { + "label": "Yandere", + "value": "7" + }, + { + "label": "Yuri", + "value": "560" + }, + { + "label": "YoungerSis", + "value": "3861" + }, + { + "label": "Yancontrol", + "value": "4235" + }, + { + "label": "Youthcampu", + "value": "4456" + }, + { + "label": "Yaoi", + "value": "247" + }, + { + "label": "Yu-Gi-Oh!", + "value": "4837" + }, + { + "label": "妖精", + "value": "3436" + }, + { + "label": "YoungerBro", + "value": "3944" + }, + { + "label": "Yugioh", + "value": "4164" + }, + { + "label": "Youth", + "value": "4762" + }, + { + "label": "直播", + "value": "3064" + }, + { + "label": "Zombies", + "value": "3693" + }, + { + "label": "Zongmen", + "value": "3402" + }, + { + "label": "Zombie", + "value": "3206" + }, + { + "label": "Zhenguan", + "value": "3445" + }, + { + "label": "Zergs", + "value": "4669" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readwn/filters/ltnovel.json b/plugins/multisrc/readwn/filters/ltnovel.json new file mode 100644 index 000000000..c223cd16d --- /dev/null +++ b/plugins/multisrc/readwn/filters/ltnovel.json @@ -0,0 +1,4132 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Sort By", + "value": "onclick", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "all", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre / Category", + "value": "", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adult", + "value": "adult" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mature", + "value": "mature" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "tags": { + "type": "Picker", + "label": "Tags", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "action", + "value": "639" + }, + { + "label": "Adventure", + "value": "657" + }, + { + "label": "Academy", + "value": "43" + }, + { + "label": "Alchemy", + "value": "46" + }, + { + "label": "ArrogantCh", + "value": "4" + }, + { + "label": "Artifacts", + "value": "127" + }, + { + "label": "Apocalypse", + "value": "206" + }, + { + "label": "AntiheroPr", + "value": "173" + }, + { + "label": "AdaptedtoM", + "value": "2" + }, + { + "label": "AlternateW", + "value": "205" + }, + { + "label": "Aristocrac", + "value": "123" + }, + { + "label": "AdaptedtoM", + "value": "167" + }, + { + "label": "ArrangedMa", + "value": "126" + }, + { + "label": "AncientChi", + "value": "164" + }, + { + "label": "AgeProgres", + "value": "208" + }, + { + "label": "Adventurer", + "value": "171" + }, + { + "label": "ArmyBuildi", + "value": "105" + }, + { + "label": "Antihero", + "value": "858" + }, + { + "label": "Assassins", + "value": "107" + }, + { + "label": "Accelerate", + "value": "163" + }, + { + "label": "AncientTim", + "value": "193" + }, + { + "label": "Appearance", + "value": "23" + }, + { + "label": "Angels", + "value": "104" + }, + { + "label": "AdaptedtoM", + "value": "249" + }, + { + "label": "Aliens", + "value": "137" + }, + { + "label": "Acting", + "value": "1" + }, + { + "label": "Amnesia", + "value": "131" + }, + { + "label": "AdaptedtoA", + "value": "144" + }, + { + "label": "AbsentPare", + "value": "250" + }, + { + "label": "AbusiveCha", + "value": "196" + }, + { + "label": "Army", + "value": "125" + }, + { + "label": "ArtifactCr", + "value": "140" + }, + { + "label": "AbilitySte", + "value": "166" + }, + { + "label": "AdaptedtoD", + "value": "145" + }, + { + "label": "AbandonedC", + "value": "213" + }, + { + "label": "ApatheticP", + "value": "169" + }, + { + "label": "AgeRegress", + "value": "283" + }, + { + "label": "Archery", + "value": "24" + }, + { + "label": "AdoptedPro", + "value": "211" + }, + { + "label": "AdoptedChi", + "value": "326" + }, + { + "label": "AdaptedtoD", + "value": "170" + }, + { + "label": "Anl", + "value": "178" + }, + { + "label": "advancedte", + "value": "804" + }, + { + "label": "Androids", + "value": "408" + }, + { + "label": "Aggressive", + "value": "440" + }, + { + "label": "Adultery", + "value": "573" + }, + { + "label": "Alpha", + "value": "853" + }, + { + "label": "Abandoned", + "value": "910" + }, + { + "label": "AdaptedtoG", + "value": "225" + }, + { + "label": "AnimalRear", + "value": "409" + }, + { + "label": "AwkwardPro", + "value": "304" + }, + { + "label": "Affair", + "value": "217" + }, + { + "label": "Automatons", + "value": "236" + }, + { + "label": "AdaptedtoM", + "value": "479" + }, + { + "label": "Artists", + "value": "492" + }, + { + "label": "AntiqueSho", + "value": "493" + }, + { + "label": "AdaptedtoV", + "value": "485" + }, + { + "label": "Anti-Magic", + "value": "527" + }, + { + "label": "ArmsDealer", + "value": "569" + }, + { + "label": "Award-winn", + "value": "593" + }, + { + "label": "Angel", + "value": "955" + }, + { + "label": "ApartmentL", + "value": "327" + }, + { + "label": "AmusementP", + "value": "462" + }, + { + "label": "Angst", + "value": "617" + }, + { + "label": "All-GirlsS", + "value": "682" + }, + { + "label": "ADeadBody", + "value": "883" + }, + { + "label": "Anime", + "value": "952" + }, + { + "label": "Androgynou", + "value": "3" + }, + { + "label": "Average-lo", + "value": "49" + }, + { + "label": "Artificial", + "value": "106" + }, + { + "label": "Appearance", + "value": "122" + }, + { + "label": "AnimalChar", + "value": "172" + }, + { + "label": "Anti-socia", + "value": "194" + }, + { + "label": "Astrologer", + "value": "419" + }, + { + "label": "Autism", + "value": "555" + }, + { + "label": "Alchemist", + "value": "616" + }, + { + "label": "AkamegaKil", + "value": "703" + }, + { + "label": "AmoralityP", + "value": "711" + }, + { + "label": "Avatar&", + "value": "788" + }, + { + "label": "Attractive", + "value": "795" + }, + { + "label": "AbsoluteDu", + "value": "824" + }, + { + "label": "ASOIAF", + "value": "838" + }, + { + "label": "APsychicDe", + "value": "885" + }, + { + "label": "BeautifulF", + "value": "5" + }, + { + "label": "Betrayal", + "value": "6" + }, + { + "label": "BeastCompa", + "value": "27" + }, + { + "label": "Bloodlines", + "value": "32" + }, + { + "label": "BodyTemper", + "value": "34" + }, + { + "label": "BusinessMa", + "value": "121" + }, + { + "label": "Beasts", + "value": "29" + }, + { + "label": "BlackBelly", + "value": "129" + }, + { + "label": "Beastkin", + "value": "179" + }, + { + "label": "BrokenEnga", + "value": "120" + }, + { + "label": "BattleAcad", + "value": "146" + }, + { + "label": "Blacksmith", + "value": "192" + }, + { + "label": "BickeringC", + "value": "218" + }, + { + "label": "Businessme", + "value": "376" + }, + { + "label": "Brotherhoo", + "value": "293" + }, + { + "label": "BattleComp", + "value": "128" + }, + { + "label": "Bullying", + "value": "141" + }, + { + "label": "BrotherCom", + "value": "311" + }, + { + "label": "Buddhism", + "value": "209" + }, + { + "label": "Books", + "value": "54" + }, + { + "label": "Blackmail", + "value": "320" + }, + { + "label": "Bookworm", + "value": "56" + }, + { + "label": "Bodyguards", + "value": "431" + }, + { + "label": "BDSM", + "value": "508" + }, + { + "label": "Beasttamin", + "value": "859" + }, + { + "label": "Bloodpumpi", + "value": "860" + }, + { + "label": "Beauty", + "value": "898" + }, + { + "label": "BloodManip", + "value": "441" + }, + { + "label": "Brainwashi", + "value": "528" + }, + { + "label": "BisexualPr", + "value": "480" + }, + { + "label": "BeautifulC", + "value": "669" + }, + { + "label": "Badboy", + "value": "881" + }, + { + "label": "Butlers", + "value": "421" + }, + { + "label": "BreastFeti", + "value": "489" + }, + { + "label": "BodySwap", + "value": "628" + }, + { + "label": "BasedonanA", + "value": "719" + }, + { + "label": "Bl", + "value": "867" + }, + { + "label": "BlindProta", + "value": "358" + }, + { + "label": "Body-doubl", + "value": "533" + }, + { + "label": "Baby", + "value": "915" + }, + { + "label": "Biochip", + "value": "471" + }, + { + "label": "Basketball", + "value": "587" + }, + { + "label": "BasedonaVi", + "value": "644" + }, + { + "label": "BasedonaMo", + "value": "770" + }, + { + "label": "Bleach", + "value": "802" + }, + { + "label": "Businesswo", + "value": "901" + }, + { + "label": "Boss-Subor", + "value": "329" + }, + { + "label": "Bands", + "value": "565" + }, + { + "label": "Baseball", + "value": "585" + }, + { + "label": "BasedonaVi", + "value": "721" + }, + { + "label": "BasedonaSo", + "value": "726" + }, + { + "label": "Beatthemal", + "value": "871" + }, + { + "label": "Beatthefem", + "value": "872" + }, + { + "label": "Bigshot", + "value": "951" + }, + { + "label": "Billionair", + "value": "975" + }, + { + "label": "Chinese", + "value": "808" + }, + { + "label": "Cultivatio", + "value": "42" + }, + { + "label": "ChineseNov", + "value": "807" + }, + { + "label": "CalmProtag", + "value": "36" + }, + { + "label": "CleverProt", + "value": "11" + }, + { + "label": "Cheats", + "value": "60" + }, + { + "label": "CharacterG", + "value": "158" + }, + { + "label": "CunningPro", + "value": "45" + }, + { + "label": "Comedy", + "value": "584" + }, + { + "label": "ColdProtag", + "value": "38" + }, + { + "label": "CaringProt", + "value": "7" + }, + { + "label": "ConfidentP", + "value": "40" + }, + { + "label": "ComedicUnd", + "value": "264" + }, + { + "label": "ColdLoveIn", + "value": "165" + }, + { + "label": "CautiousPr", + "value": "157" + }, + { + "label": "Childcare", + "value": "9" + }, + { + "label": "Cooking", + "value": "245" + }, + { + "label": "CarefreePr", + "value": "219" + }, + { + "label": "Celebritie", + "value": "8" + }, + { + "label": "CharmingPr", + "value": "59" + }, + { + "label": "ChildhoodF", + "value": "240" + }, + { + "label": "Crafting", + "value": "108" + }, + { + "label": "CuteProtag", + "value": "248" + }, + { + "label": "CuteChildr", + "value": "14" + }, + { + "label": "CoupleGrow", + "value": "214" + }, + { + "label": "CEO", + "value": "623" + }, + { + "label": "CruelChara", + "value": "284" + }, + { + "label": "ChildProta", + "value": "210" + }, + { + "label": "Conquer", + "value": "863" + }, + { + "label": "Crime", + "value": "220" + }, + { + "label": "ClingyLove", + "value": "180" + }, + { + "label": "Contracts", + "value": "312" + }, + { + "label": "ClanBuildi", + "value": "159" + }, + { + "label": "ChildhoodL", + "value": "10" + }, + { + "label": "Campus", + "value": "927" + }, + { + "label": "Curses", + "value": "328" + }, + { + "label": "Cross-dres", + "value": "13" + }, + { + "label": "ChildAbuse", + "value": "331" + }, + { + "label": "Corruption", + "value": "138" + }, + { + "label": "CuteStory", + "value": "496" + }, + { + "label": "Cannibalis", + "value": "294" + }, + { + "label": "CollegeorU", + "value": "224" + }, + { + "label": "Cohabitati", + "value": "404" + }, + { + "label": "Clones", + "value": "61" + }, + { + "label": "CuriousPro", + "value": "720" + }, + { + "label": "ChatRooms", + "value": "383" + }, + { + "label": "CosmicWars", + "value": "422" + }, + { + "label": "Cnnilingus", + "value": "509" + }, + { + "label": "Criminals", + "value": "517" + }, + { + "label": "Conditiona", + "value": "490" + }, + { + "label": "CowardlyPr", + "value": "174" + }, + { + "label": "Chefs", + "value": "359" + }, + { + "label": "ChildishPr", + "value": "515" + }, + { + "label": "Cousins", + "value": "181" + }, + { + "label": "ComingofAg", + "value": "464" + }, + { + "label": "Clubs", + "value": "576" + }, + { + "label": "ChildhoodP", + "value": "318" + }, + { + "label": "Conflictin", + "value": "458" + }, + { + "label": "CourtOffic", + "value": "465" + }, + { + "label": "CardGames", + "value": "478" + }, + { + "label": "ClumsyLove", + "value": "499" + }, + { + "label": "Coma", + "value": "544" + }, + { + "label": "Co-Workers", + "value": "541" + }, + { + "label": "Cheat", + "value": "771" + }, + { + "label": "Crossover", + "value": "366" + }, + { + "label": "Chuunibyou", + "value": "474" + }, + { + "label": "CollegeUni", + "value": "536" + }, + { + "label": "Confinemen", + "value": "684" + }, + { + "label": "CharacterD", + "value": "742" + }, + { + "label": "Childhoods", + "value": "870" + }, + { + "label": "Crossdress", + "value": "933" + }, + { + "label": "ComplexFam", + "value": "12" + }, + { + "label": "Charismati", + "value": "58" + }, + { + "label": "Cryostasis", + "value": "553" + }, + { + "label": "Chatgroup", + "value": "608" + }, + { + "label": "ChaptersRe", + "value": "687" + }, + { + "label": "Charlotte(", + "value": "704" + }, + { + "label": "Cyberpunk", + "value": "792" + }, + { + "label": "Chivalryof", + "value": "823" + }, + { + "label": "CatchaGhos", + "value": "893" + }, + { + "label": "Crush", + "value": "919" + }, + { + "label": "Contractma", + "value": "930" + }, + { + "label": "Coolguy", + "value": "966" + }, + { + "label": "Counteratt", + "value": "968" + }, + { + "label": "ClassroomO", + "value": "969" + }, + { + "label": "Demons", + "value": "65" + }, + { + "label": "Dark", + "value": "317" + }, + { + "label": "Dragons", + "value": "48" + }, + { + "label": "DevotedLov", + "value": "70" + }, + { + "label": "Dungeons", + "value": "110" + }, + { + "label": "DenseProta", + "value": "67" + }, + { + "label": "DotingLove", + "value": "15" + }, + { + "label": "Depictions", + "value": "130" + }, + { + "label": "DeathofLov", + "value": "139" + }, + { + "label": "DemonLord", + "value": "147" + }, + { + "label": "Demi-Human", + "value": "226" + }, + { + "label": "Doctors", + "value": "72" + }, + { + "label": "Dwarfs", + "value": "111" + }, + { + "label": "Death", + "value": "406" + }, + { + "label": "DaoCompreh", + "value": "47" + }, + { + "label": "DotingPare", + "value": "360" + }, + { + "label": "DotingOlde", + "value": "227" + }, + { + "label": "Discrimina", + "value": "132" + }, + { + "label": "Dragon", + "value": "610" + }, + { + "label": "Detectives", + "value": "221" + }, + { + "label": "DomesticAf", + "value": "246" + }, + { + "label": "Destiny", + "value": "305" + }, + { + "label": "Divorce", + "value": "215" + }, + { + "label": "Disabiliti", + "value": "633" + }, + { + "label": "Depression", + "value": "306" + }, + { + "label": "DragonSlay", + "value": "361" + }, + { + "label": "DivineProt", + "value": "399" + }, + { + "label": "DungeonMas", + "value": "502" + }, + { + "label": "Daoism", + "value": "212" + }, + { + "label": "Devil", + "value": "934" + }, + { + "label": "DaoCompani", + "value": "168" + }, + { + "label": "Devils", + "value": "613" + }, + { + "label": "Dreams", + "value": "477" + }, + { + "label": "Delinquent", + "value": "675" + }, + { + "label": "Dramatic", + "value": "926" + }, + { + "label": "Divination", + "value": "384" + }, + { + "label": "Drugs", + "value": "390" + }, + { + "label": "Druids", + "value": "395" + }, + { + "label": "Debts", + "value": "562" + }, + { + "label": "Dystopia", + "value": "564" + }, + { + "label": "DishonestP", + "value": "643" + }, + { + "label": "DragonRide", + "value": "668" + }, + { + "label": "Doctor", + "value": "902" + }, + { + "label": "DollsorPup", + "value": "579" + }, + { + "label": "Disfigurem", + "value": "420" + }, + { + "label": "DeadProtag", + "value": "698" + }, + { + "label": "Determined", + "value": "109" + }, + { + "label": "DemonicCul", + "value": "195" + }, + { + "label": "Delusions", + "value": "443" + }, + { + "label": "DifferentS", + "value": "466" + }, + { + "label": "Dancers", + "value": "566" + }, + { + "label": "doppelgang", + "value": "625" + }, + { + "label": "Distrustfu", + "value": "653" + }, + { + "label": "Divination", + "value": "685" + }, + { + "label": "DoulouDalu", + "value": "700" + }, + { + "label": "Diplomacy", + "value": "728" + }, + { + "label": "Dwarves", + "value": "740" + }, + { + "label": "DouluoDalu", + "value": "787" + }, + { + "label": "DanMachi", + "value": "789" + }, + { + "label": "DragonBall", + "value": "791" + }, + { + "label": "Detailed", + "value": "900" + }, + { + "label": "Detective", + "value": "970" + }, + { + "label": "Evolution", + "value": "265" + }, + { + "label": "Elves", + "value": "113" + }, + { + "label": "EuropeanAm", + "value": "261" + }, + { + "label": "ElementalM", + "value": "160" + }, + { + "label": "EarlyRoman", + "value": "133" + }, + { + "label": "EvilProtag", + "value": "411" + }, + { + "label": "EasternSet", + "value": "597" + }, + { + "label": "EvilGods", + "value": "232" + }, + { + "label": "EyePowers", + "value": "51" + }, + { + "label": "EnemiesBec", + "value": "175" + }, + { + "label": "EideticMem", + "value": "50" + }, + { + "label": "Empires", + "value": "182" + }, + { + "label": "EnemiesBec", + "value": "134" + }, + { + "label": "EvilReligi", + "value": "161" + }, + { + "label": "EvilOrgani", + "value": "207" + }, + { + "label": "Economics", + "value": "112" + }, + { + "label": "Engagement", + "value": "307" + }, + { + "label": "Ecchi", + "value": "956" + }, + { + "label": "Episodic", + "value": "222" + }, + { + "label": "Exorcism", + "value": "646" + }, + { + "label": "Enemiestol", + "value": "875" + }, + { + "label": "EasyGoingL", + "value": "400" + }, + { + "label": "Egoist", + "value": "957" + }, + { + "label": "Entertainm", + "value": "830" + }, + { + "label": "e-Sports", + "value": "271" + }, + { + "label": "Exhibition", + "value": "511" + }, + { + "label": "Enlightenm", + "value": "75" + }, + { + "label": "EarthInvas", + "value": "423" + }, + { + "label": "ElderlyPro", + "value": "649" + }, + { + "label": "Engineer", + "value": "135" + }, + { + "label": "Emotionall", + "value": "321" + }, + { + "label": "Elementali", + "value": "622" + }, + { + "label": "Eunuch", + "value": "753" + }, + { + "label": "EvilSpirit", + "value": "892" + }, + { + "label": "Ex", + "value": "908" + }, + { + "label": "Esper", + "value": "976" + }, + { + "label": "FemaleProt", + "value": "16" + }, + { + "label": "FantasyWor", + "value": "136" + }, + { + "label": "FastCultiv", + "value": "78" + }, + { + "label": "Fanfiction", + "value": "282" + }, + { + "label": "Friendship", + "value": "177" + }, + { + "label": "First-time", + "value": "183" + }, + { + "label": "FamousProt", + "value": "176" + }, + { + "label": "FamilialLo", + "value": "347" + }, + { + "label": "FantasyCre", + "value": "115" + }, + { + "label": "faceslappi", + "value": "632" + }, + { + "label": "FastLearne", + "value": "80" + }, + { + "label": "Family", + "value": "354" + }, + { + "label": "Futuristic", + "value": "308" + }, + { + "label": "FirstLove", + "value": "279" + }, + { + "label": "Firearms", + "value": "17" + }, + { + "label": "FamilyConf", + "value": "335" + }, + { + "label": "Farming", + "value": "247" + }, + { + "label": "Fairies", + "value": "114" + }, + { + "label": "Fllatio", + "value": "184" + }, + { + "label": "FearlessPr", + "value": "497" + }, + { + "label": "FatedLover", + "value": "116" + }, + { + "label": "FamousPare", + "value": "438" + }, + { + "label": "Fastpaced", + "value": "938" + }, + { + "label": "ForcedMarr", + "value": "198" + }, + { + "label": "FattoFit", + "value": "488" + }, + { + "label": "Familiars", + "value": "556" + }, + { + "label": "Filipino", + "value": "813" + }, + { + "label": "Future", + "value": "868" + }, + { + "label": "FemaleMast", + "value": "510" + }, + { + "label": "FilipinoNo", + "value": "812" + }, + { + "label": "Fatedlove", + "value": "888" + }, + { + "label": "FamilyBusi", + "value": "437" + }, + { + "label": "Forbiddenl", + "value": "899" + }, + { + "label": "FormerHero", + "value": "627" + }, + { + "label": "FriendsBec", + "value": "407" + }, + { + "label": "FleetBattl", + "value": "424" + }, + { + "label": "Flashbacks", + "value": "459" + }, + { + "label": "FoxSpirits", + "value": "531" + }, + { + "label": "FatProtago", + "value": "567" + }, + { + "label": "FemaleLead", + "value": "575" + }, + { + "label": "FallenAnge", + "value": "618" + }, + { + "label": "FallenNobi", + "value": "619" + }, + { + "label": "Fantasy", + "value": "781" + }, + { + "label": "FairyTail", + "value": "674" + }, + { + "label": "FemaletoMa", + "value": "727" + }, + { + "label": "FantasyMag", + "value": "748" + }, + { + "label": "FengShui", + "value": "762" + }, + { + "label": "Friendstol", + "value": "903" + }, + { + "label": "Forcedinto", + "value": "197" + }, + { + "label": "Folklore", + "value": "460" + }, + { + "label": "Futanari", + "value": "512" + }, + { + "label": "FoodShopke", + "value": "592" + }, + { + "label": "Fellatio", + "value": "723" + }, + { + "label": "Fan-fictio", + "value": "755" + }, + { + "label": "First-time", + "value": "765" + }, + { + "label": "Fatestayni", + "value": "834" + }, + { + "label": "Fantasyrom", + "value": "939" + }, + { + "label": "Fastpace", + "value": "967" + }, + { + "label": "Fiction", + "value": "974" + }, + { + "label": "GameElemen", + "value": "117" + }, + { + "label": "GeniusProt", + "value": "143" + }, + { + "label": "Gods", + "value": "52" + }, + { + "label": "Guilds", + "value": "119" + }, + { + "label": "Gore", + "value": "237" + }, + { + "label": "Gamers", + "value": "272" + }, + { + "label": "Genius", + "value": "862" + }, + { + "label": "Ghosts", + "value": "367" + }, + { + "label": "Goddesses", + "value": "336" + }, + { + "label": "GatetoAnot", + "value": "337" + }, + { + "label": "GameRankin", + "value": "118" + }, + { + "label": "GodlyPower", + "value": "162" + }, + { + "label": "GodProtago", + "value": "287" + }, + { + "label": "GeneticMod", + "value": "295" + }, + { + "label": "Grinding", + "value": "377" + }, + { + "label": "Gangs", + "value": "142" + }, + { + "label": "Goblins", + "value": "346" + }, + { + "label": "Gunfighter", + "value": "482" + }, + { + "label": "Golems", + "value": "270" + }, + { + "label": "Generals", + "value": "369" + }, + { + "label": "Grimdark", + "value": "827" + }, + { + "label": "Gambling", + "value": "330" + }, + { + "label": "Gladiators", + "value": "534" + }, + { + "label": "GenderBend", + "value": "634" + }, + { + "label": "Galge", + "value": "741" + }, + { + "label": "Game", + "value": "769" + }, + { + "label": "GameofThro", + "value": "837" + }, + { + "label": "Gettingbac", + "value": "907" + }, + { + "label": "God-humanR", + "value": "185" + }, + { + "label": "GraveKeepe", + "value": "647" + }, + { + "label": "Genderless", + "value": "699" + }, + { + "label": "Girl&03", + "value": "746" + }, + { + "label": "Glasses-we", + "value": "778" + }, + { + "label": "GameLit", + "value": "844" + }, + { + "label": "GhostEvent", + "value": "886" + }, + { + "label": "HandsomeMa", + "value": "18" + }, + { + "label": "Harem", + "value": "650" + }, + { + "label": "HidingTrue", + "value": "57" + }, + { + "label": "HidingTrue", + "value": "251" + }, + { + "label": "HiddenAbil", + "value": "333" + }, + { + "label": "HumanoidPr", + "value": "289" + }, + { + "label": "Heroes", + "value": "288" + }, + { + "label": "Heartwarmi", + "value": "362" + }, + { + "label": "Hunters", + "value": "266" + }, + { + "label": "Historical", + "value": "874" + }, + { + "label": "Highiq", + "value": "920" + }, + { + "label": "HeavenlyTr", + "value": "148" + }, + { + "label": "HiddenGem", + "value": "538" + }, + { + "label": "HumanExper", + "value": "296" + }, + { + "label": "HighFantas", + "value": "821" + }, + { + "label": "Hackers", + "value": "216" + }, + { + "label": "HatedProta", + "value": "199" + }, + { + "label": "HonestProt", + "value": "385" + }, + { + "label": "Healers", + "value": "500" + }, + { + "label": "Hndjob", + "value": "187" + }, + { + "label": "Hell", + "value": "503" + }, + { + "label": "HarryPotte", + "value": "715" + }, + { + "label": "Healing", + "value": "760" + }, + { + "label": "Hypnotism", + "value": "392" + }, + { + "label": "HelpfulPro", + "value": "453" + }, + { + "label": "Heterochro", + "value": "55" + }, + { + "label": "HarshTrain", + "value": "309" + }, + { + "label": "Heaven", + "value": "620" + }, + { + "label": "HighSchool", + "value": "702" + }, + { + "label": "Hospital", + "value": "535" + }, + { + "label": "HumanWeapo", + "value": "310" + }, + { + "label": "Herbalist", + "value": "442" + }, + { + "label": "Horror", + "value": "971" + }, + { + "label": "Hard-Worki", + "value": "53" + }, + { + "label": "Harem-seek", + "value": "186" + }, + { + "label": "Half-human", + "value": "235" + }, + { + "label": "Human-Nonh", + "value": "391" + }, + { + "label": "Hot-bloode", + "value": "413" + }, + { + "label": "Hentai", + "value": "532" + }, + { + "label": "Handjob", + "value": "545" + }, + { + "label": "history", + "value": "724" + }, + { + "label": "Hokage", + "value": "756" + }, + { + "label": "Hunter×Hu", + "value": "767" + }, + { + "label": "HardSci-fi", + "value": "839" + }, + { + "label": "Heartthrob", + "value": "911" + }, + { + "label": "Hiddenmarr", + "value": "943" + }, + { + "label": "Isekai", + "value": "833" + }, + { + "label": "Immortals", + "value": "252" + }, + { + "label": "Incest", + "value": "355" + }, + { + "label": "Inheritanc", + "value": "149" + }, + { + "label": "Immortal", + "value": "851" + }, + { + "label": "Invincible", + "value": "954" + }, + { + "label": "Industrial", + "value": "468" + }, + { + "label": "Interstell", + "value": "963" + }, + { + "label": "ImperialHa", + "value": "374" + }, + { + "label": "Indonesia", + "value": "819" + }, + { + "label": "Investigat", + "value": "223" + }, + { + "label": "Inscriptio", + "value": "150" + }, + { + "label": "IndonesiaN", + "value": "818" + }, + { + "label": "Insects", + "value": "338" + }, + { + "label": "Inferiorit", + "value": "447" + }, + { + "label": "Interconne", + "value": "290" + }, + { + "label": "Introverte", + "value": "291" + }, + { + "label": "Interdimen", + "value": "425" + }, + { + "label": "Invisibili", + "value": "641" + }, + { + "label": "Incubus", + "value": "663" + }, + { + "label": "IsItWrongt", + "value": "825" + }, + { + "label": "IdentityCr", + "value": "831" + }, + { + "label": "Imposter", + "value": "854" + }, + { + "label": "Japanese", + "value": "806" + }, + { + "label": "Jealousy", + "value": "124" + }, + { + "label": "JackofAllT", + "value": "82" + }, + { + "label": "Korean", + "value": "810" + }, + { + "label": "KingdomBui", + "value": "486" + }, + { + "label": "KoreanNove", + "value": "811" + }, + { + "label": "Kingdoms", + "value": "253" + }, + { + "label": "Knights", + "value": "188" + }, + { + "label": "Kidnapping", + "value": "285" + }, + { + "label": "KindLoveIn", + "value": "348" + }, + { + "label": "Killer", + "value": "856" + }, + { + "label": "Kuudere", + "value": "501" + }, + { + "label": "Kendo", + "value": "604" + }, + { + "label": "Karma", + "value": "614" + }, + { + "label": "Kakashi", + "value": "757" + }, + { + "label": "LightNovel", + "value": "809" + }, + { + "label": "LevelSyste", + "value": "239" + }, + { + "label": "LitRPG", + "value": "539" + }, + { + "label": "Levelup", + "value": "847" + }, + { + "label": "LuckyProta", + "value": "21" + }, + { + "label": "Loli", + "value": "189" + }, + { + "label": "LoyalSubor", + "value": "241" + }, + { + "label": "LazyProtag", + "value": "254" + }, + { + "label": "LateRomanc", + "value": "19" + }, + { + "label": "LackofComm", + "value": "439" + }, + { + "label": "Low-keyPro", + "value": "313" + }, + { + "label": "LongSepara", + "value": "190" + }, + { + "label": "Leadership", + "value": "238" + }, + { + "label": "LoveatFirs", + "value": "461" + }, + { + "label": "LoveTriang", + "value": "349" + }, + { + "label": "LonerProta", + "value": "426" + }, + { + "label": "Lovetriang", + "value": "905" + }, + { + "label": "Legends", + "value": "151" + }, + { + "label": "Library", + "value": "85" + }, + { + "label": "Lolicon", + "value": "286" + }, + { + "label": "LimitedLif", + "value": "86" + }, + { + "label": "Lottery", + "value": "563" + }, + { + "label": "LoversReun", + "value": "350" + }, + { + "label": "LGBTQA", + "value": "601" + }, + { + "label": "LoveRivals", + "value": "676" + }, + { + "label": "LowFantasy", + "value": "796" + }, + { + "label": "LoveIntere", + "value": "20" + }, + { + "label": "LostCivili", + "value": "455" + }, + { + "label": "LittleRoma", + "value": "581" + }, + { + "label": "Loneliness", + "value": "694" + }, + { + "label": "leonine", + "value": "695" + }, + { + "label": "LivingAlon", + "value": "733" + }, + { + "label": "Littlebun", + "value": "931" + }, + { + "label": "Loveafterm", + "value": "962" + }, + { + "label": "MaleProtag", + "value": "63" + }, + { + "label": "Magic", + "value": "292" + }, + { + "label": "ModernDay", + "value": "25" + }, + { + "label": "Monsters", + "value": "64" + }, + { + "label": "Mystery", + "value": "848" + }, + { + "label": "MultiplePO", + "value": "258" + }, + { + "label": "Misunderst", + "value": "22" + }, + { + "label": "ModernKnow", + "value": "274" + }, + { + "label": "MultipleRe", + "value": "66" + }, + { + "label": "Military", + "value": "257" + }, + { + "label": "MMORPG", + "value": "454" + }, + { + "label": "Marriage", + "value": "191" + }, + { + "label": "MagicBeast", + "value": "339" + }, + { + "label": "MoneyGrubb", + "value": "90" + }, + { + "label": "MagicalTec", + "value": "273" + }, + { + "label": "MagicForma", + "value": "62" + }, + { + "label": "Mythology", + "value": "469" + }, + { + "label": "MagicalSpa", + "value": "228" + }, + { + "label": "Mysterious", + "value": "298" + }, + { + "label": "MatureProt", + "value": "256" + }, + { + "label": "MythicalBe", + "value": "356" + }, + { + "label": "Medieval", + "value": "580" + }, + { + "label": "MonsterTam", + "value": "267" + }, + { + "label": "MedicalKno", + "value": "351" + }, + { + "label": "MutatedCre", + "value": "268" + }, + { + "label": "MaletoFema", + "value": "635" + }, + { + "label": "Maids", + "value": "255" + }, + { + "label": "Management", + "value": "487" + }, + { + "label": "Mafia", + "value": "877" + }, + { + "label": "Myth", + "value": "924" + }, + { + "label": "Music", + "value": "332" + }, + { + "label": "MaleYander", + "value": "401" + }, + { + "label": "MultiplePr", + "value": "655" + }, + { + "label": "MysterySol", + "value": "578" + }, + { + "label": "Mercenarie", + "value": "314" + }, + { + "label": "MultipleId", + "value": "345" + }, + { + "label": "Martialart", + "value": "549" + }, + { + "label": "Murders", + "value": "636" + }, + { + "label": "Movies", + "value": "26" + }, + { + "label": "Mutations", + "value": "297" + }, + { + "label": "Mecha", + "value": "394" + }, + { + "label": "Marvel", + "value": "651" + }, + { + "label": "Mutation", + "value": "945" + }, + { + "label": "Merchants", + "value": "557" + }, + { + "label": "MindContro", + "value": "577" + }, + { + "label": "MonsterGir", + "value": "664" + }, + { + "label": "Malaysian", + "value": "817" + }, + { + "label": "MartialSpi", + "value": "414" + }, + { + "label": "MagicalGir", + "value": "543" + }, + { + "label": "MobProtago", + "value": "734" + }, + { + "label": "ModernWorl", + "value": "686" + }, + { + "label": "ModernFant", + "value": "688" + }, + { + "label": "Mysterious", + "value": "526" + }, + { + "label": "Mpreg", + "value": "546" + }, + { + "label": "MuteCharac", + "value": "568" + }, + { + "label": "MagicAcade", + "value": "590" + }, + { + "label": "Msturbatio", + "value": "677" + }, + { + "label": "ManlyGayCo", + "value": "754" + }, + { + "label": "MaleLead", + "value": "797" + }, + { + "label": "Mythos", + "value": "799" + }, + { + "label": "MalaysianN", + "value": "816" + }, + { + "label": "Multiplele", + "value": "942" + }, + { + "label": "Manipulati", + "value": "87" + }, + { + "label": "Master-Dis", + "value": "88" + }, + { + "label": "Master-Ser", + "value": "89" + }, + { + "label": "Mysterious", + "value": "152" + }, + { + "label": "Models", + "value": "280" + }, + { + "label": "MultipleRe", + "value": "463" + }, + { + "label": "Masochisti", + "value": "523" + }, + { + "label": "MultipleTr", + "value": "558" + }, + { + "label": "Mage", + "value": "631" + }, + { + "label": "Masturbati", + "value": "671" + }, + { + "label": "Multiverse", + "value": "681" + }, + { + "label": "Marriageof", + "value": "718" + }, + { + "label": "MonsterSoc", + "value": "739" + }, + { + "label": "MyHeroAcad", + "value": "751" + }, + { + "label": "MultipleTi", + "value": "761" + }, + { + "label": "Massacre", + "value": "790" + }, + { + "label": "MultipleLe", + "value": "798" + }, + { + "label": "MultiplePe", + "value": "801" + }, + { + "label": "Mysterious", + "value": "882" + }, + { + "label": "Modern", + "value": "909" + }, + { + "label": "Marysue", + "value": "921" + }, + { + "label": "Mature", + "value": "928" + }, + { + "label": "Mag", + "value": "953" + }, + { + "label": "Monster", + "value": "960" + }, + { + "label": "ModernLife", + "value": "972" + }, + { + "label": "Nobles", + "value": "259" + }, + { + "label": "Non-humanP", + "value": "665" + }, + { + "label": "NaiveProta", + "value": "322" + }, + { + "label": "Nationalis", + "value": "262" + }, + { + "label": "Necromance", + "value": "315" + }, + { + "label": "NotHarem", + "value": "599" + }, + { + "label": "NA", + "value": "814" + }, + { + "label": "Nonhuman", + "value": "904" + }, + { + "label": "No-Harem", + "value": "935" + }, + { + "label": "Netori", + "value": "498" + }, + { + "label": "Naruto", + "value": "652" + }, + { + "label": "NoRomance", + "value": "642" + }, + { + "label": "Near-Death", + "value": "28" + }, + { + "label": "Ninjas", + "value": "521" + }, + { + "label": "NPC", + "value": "842" + }, + { + "label": "Nudity", + "value": "472" + }, + { + "label": "Netorare", + "value": "707" + }, + { + "label": "NoCheats", + "value": "845" + }, + { + "label": "Non-humano", + "value": "427" + }, + { + "label": "Narcissist", + "value": "432" + }, + { + "label": "NotYaoi", + "value": "691" + }, + { + "label": "Neet", + "value": "758" + }, + { + "label": "Nurses", + "value": "766" + }, + { + "label": "Non-Humanl", + "value": "822" + }, + { + "label": "Overpowere", + "value": "846" + }, + { + "label": "OlderLoveI", + "value": "396" + }, + { + "label": "Orphans", + "value": "397" + }, + { + "label": "Orcs", + "value": "363" + }, + { + "label": "OuterSpace", + "value": "378" + }, + { + "label": "OrganizedC", + "value": "380" + }, + { + "label": "OtomeGame", + "value": "735" + }, + { + "label": "ordinary", + "value": "747" + }, + { + "label": "OnePiece", + "value": "785" + }, + { + "label": "ObsessiveL", + "value": "494" + }, + { + "label": "OfficeRoma", + "value": "648" + }, + { + "label": "Overlord", + "value": "743" + }, + { + "label": "Onenightst", + "value": "917" + }, + { + "label": "Overpowere", + "value": "91" + }, + { + "label": "Outcasts", + "value": "583" + }, + { + "label": "Overprotec", + "value": "729" + }, + { + "label": "OriginalON", + "value": "793" + }, + { + "label": "Omegaverse", + "value": "918" + }, + { + "label": "Overpowere", + "value": "961" + }, + { + "label": "PoortoRich", + "value": "229" + }, + { + "label": "Polygamy", + "value": "71" + }, + { + "label": "PowerCoupl", + "value": "33" + }, + { + "label": "Politics", + "value": "242" + }, + { + "label": "Pregnancy", + "value": "35" + }, + { + "label": "Pets", + "value": "69" + }, + { + "label": "Possessive", + "value": "31" + }, + { + "label": "Post-apoca", + "value": "299" + }, + { + "label": "Possessive", + "value": "850" + }, + { + "label": "Powerfulco", + "value": "852" + }, + { + "label": "PastPlaysa", + "value": "200" + }, + { + "label": "PoorProtag", + "value": "476" + }, + { + "label": "ProactiveP", + "value": "316" + }, + { + "label": "Parody", + "value": "386" + }, + { + "label": "PreviousLi", + "value": "467" + }, + { + "label": "PervertedP", + "value": "448" + }, + { + "label": "PillConcoc", + "value": "92" + }, + { + "label": "PsychicPow", + "value": "260" + }, + { + "label": "Police", + "value": "518" + }, + { + "label": "ParallelWo", + "value": "507" + }, + { + "label": "PastTrauma", + "value": "201" + }, + { + "label": "Psychopath", + "value": "561" + }, + { + "label": "PragmaticP", + "value": "660" + }, + { + "label": "Princess", + "value": "879" + }, + { + "label": "Personalit", + "value": "68" + }, + { + "label": "Poisons", + "value": "93" + }, + { + "label": "Phoenixes", + "value": "153" + }, + { + "label": "Prison", + "value": "444" + }, + { + "label": "Pirates", + "value": "547" + }, + { + "label": "Priests", + "value": "654" + }, + { + "label": "PlayfulPro", + "value": "701" + }, + { + "label": "Pokemon", + "value": "779" + }, + { + "label": "PortalFant", + "value": "836" + }, + { + "label": "PopularLov", + "value": "30" + }, + { + "label": "Possession", + "value": "357" + }, + { + "label": "PowerStrug", + "value": "370" + }, + { + "label": "Progressio", + "value": "800" + }, + { + "label": "Positive", + "value": "948" + }, + { + "label": "ParentComp", + "value": "393" + }, + { + "label": "Prophecies", + "value": "572" + }, + { + "label": "Programmer", + "value": "667" + }, + { + "label": "Philosophi", + "value": "456" + }, + { + "label": "Protagonis", + "value": "705" + }, + { + "label": "Psychologi", + "value": "713" + }, + { + "label": "Priestesse", + "value": "717" + }, + { + "label": "Protagonis", + "value": "94" + }, + { + "label": "Protagonis", + "value": "381" + }, + { + "label": "Protagonis", + "value": "403" + }, + { + "label": "PillBasedC", + "value": "415" + }, + { + "label": "Polyandry", + "value": "519" + }, + { + "label": "PreviousLi", + "value": "529" + }, + { + "label": "Pharmacist", + "value": "574" + }, + { + "label": "Planets", + "value": "607" + }, + { + "label": "Parasites", + "value": "629" + }, + { + "label": "Playboys", + "value": "630" + }, + { + "label": "Paizuri", + "value": "670" + }, + { + "label": "Precogniti", + "value": "710" + }, + { + "label": "Protagonis", + "value": "737" + }, + { + "label": "PacifistPr", + "value": "744" + }, + { + "label": "Persistent", + "value": "749" + }, + { + "label": "Pilots", + "value": "780" + }, + { + "label": "Popular", + "value": "894" + }, + { + "label": "PrettyGirl", + "value": "895" + }, + { + "label": "QuirkyChar", + "value": "387" + }, + { + "label": "Reincarnat", + "value": "74" + }, + { + "label": "R18", + "value": "855" + }, + { + "label": "Romance", + "value": "638" + }, + { + "label": "R-18", + "value": "417" + }, + { + "label": "Revenge", + "value": "37" + }, + { + "label": "RomanticSu", + "value": "154" + }, + { + "label": "RuthlessPr", + "value": "76" + }, + { + "label": "Royalty", + "value": "243" + }, + { + "label": "Rpe", + "value": "203" + }, + { + "label": "Racism", + "value": "379" + }, + { + "label": "Rebirth", + "value": "491" + }, + { + "label": "Religions", + "value": "398" + }, + { + "label": "R-15", + "value": "559" + }, + { + "label": "Royalfamil", + "value": "873" + }, + { + "label": "Rarebloodl", + "value": "864" + }, + { + "label": "Rape", + "value": "530" + }, + { + "label": "Restaurant", + "value": "364" + }, + { + "label": "RighteousP", + "value": "540" + }, + { + "label": "Resurrecti", + "value": "552" + }, + { + "label": "RankSystem", + "value": "595" + }, + { + "label": "RaceChange", + "value": "73" + }, + { + "label": "ReverseRpe", + "value": "483" + }, + { + "label": "ReverseRap", + "value": "690" + }, + { + "label": "Rebellion", + "value": "750" + }, + { + "label": "RichProtag", + "value": "803" + }, + { + "label": "Richfamily", + "value": "889" + }, + { + "label": "Reincarnat", + "value": "95" + }, + { + "label": "RpeVictimB", + "value": "202" + }, + { + "label": "Returningf", + "value": "412" + }, + { + "label": "Reincarnat", + "value": "428" + }, + { + "label": "Raids", + "value": "436" + }, + { + "label": "ReverseHar", + "value": "520" + }, + { + "label": "Reincarnat", + "value": "605" + }, + { + "label": "Reincarnat", + "value": "689" + }, + { + "label": "ResolutePr", + "value": "697" + }, + { + "label": "Reincarnat", + "value": "722" + }, + { + "label": "RWBY", + "value": "829" + }, + { + "label": "Races", + "value": "841" + }, + { + "label": "Righteous", + "value": "947" + }, + { + "label": "System", + "value": "537" + }, + { + "label": "SpecialAbi", + "value": "269" + }, + { + "label": "SwordAndMa", + "value": "344" + }, + { + "label": "Superpower", + "value": "782" + }, + { + "label": "SliceofLif", + "value": "621" + }, + { + "label": "Survival", + "value": "302" + }, + { + "label": "SecondChan", + "value": "281" + }, + { + "label": "SystemAdmi", + "value": "277" + }, + { + "label": "StrongtoSt", + "value": "450" + }, + { + "label": "StrongLove", + "value": "156" + }, + { + "label": "SlowRomanc", + "value": "231" + }, + { + "label": "ShamelessP", + "value": "97" + }, + { + "label": "SecretIden", + "value": "96" + }, + { + "label": "SwordWield", + "value": "100" + }, + { + "label": "Showbiz", + "value": "39" + }, + { + "label": "SummoningM", + "value": "343" + }, + { + "label": "Strategist", + "value": "435" + }, + { + "label": "Smut", + "value": "516" + }, + { + "label": "Sweetlove", + "value": "865" + }, + { + "label": "SurvivalGa", + "value": "368" + }, + { + "label": "Spirits", + "value": "372" + }, + { + "label": "Sects", + "value": "410" + }, + { + "label": "SlowCultiv", + "value": "624" + }, + { + "label": "SkillAssim", + "value": "371" + }, + { + "label": "SkillCreat", + "value": "352" + }, + { + "label": "StrategicB", + "value": "373" + }, + { + "label": "Summons", + "value": "913" + }, + { + "label": "SuddenStre", + "value": "77" + }, + { + "label": "SlowGrowth", + "value": "341" + }, + { + "label": "SoulPower", + "value": "452" + }, + { + "label": "Spaceship", + "value": "600" + }, + { + "label": "Souls", + "value": "99" + }, + { + "label": "Saves", + "value": "300" + }, + { + "label": "StoreOwner", + "value": "276" + }, + { + "label": "SentientOb", + "value": "353" + }, + { + "label": "SectDevelo", + "value": "475" + }, + { + "label": "SecretOrga", + "value": "548" + }, + { + "label": "Space", + "value": "716" + }, + { + "label": "SummonedHe", + "value": "560" + }, + { + "label": "Sweet", + "value": "916" + }, + { + "label": "SmartCoupl", + "value": "41" + }, + { + "label": "SpatialMan", + "value": "233" + }, + { + "label": "Scientists", + "value": "388" + }, + { + "label": "SkillBooks", + "value": "434" + }, + { + "label": "Shapeshift", + "value": "944" + }, + { + "label": "ShyCharact", + "value": "98" + }, + { + "label": "SealedPowe", + "value": "155" + }, + { + "label": "SuddenWeal", + "value": "342" + }, + { + "label": "SelfishPro", + "value": "470" + }, + { + "label": "Siblings", + "value": "505" + }, + { + "label": "Slaves", + "value": "678" + }, + { + "label": "SuperHeroe", + "value": "725" + }, + { + "label": "Scary", + "value": "937" + }, + { + "label": "Singlefema", + "value": "946" + }, + { + "label": "SportsBask", + "value": "588" + }, + { + "label": "SpiritUser", + "value": "645" + }, + { + "label": "Sci-fi", + "value": "679" + }, + { + "label": "Superstar", + "value": "922" + }, + { + "label": "SxualAbuse", + "value": "204" + }, + { + "label": "Secrets", + "value": "319" + }, + { + "label": "SinglePare", + "value": "365" + }, + { + "label": "Soldiers", + "value": "449" + }, + { + "label": "Saints", + "value": "481" + }, + { + "label": "SpiritAdvi", + "value": "484" + }, + { + "label": "SpearWield", + "value": "524" + }, + { + "label": "Suicides", + "value": "656" + }, + { + "label": "Seduction", + "value": "672" + }, + { + "label": "Succubus", + "value": "673" + }, + { + "label": "Samurai", + "value": "683" + }, + { + "label": "SadisticCh", + "value": "692" + }, + { + "label": "SicklyChar", + "value": "712" + }, + { + "label": "SelflessPr", + "value": "738" + }, + { + "label": "Satire", + "value": "835" + }, + { + "label": "Serious", + "value": "936" + }, + { + "label": "Seductive", + "value": "949" + }, + { + "label": "Supernatur", + "value": "964" + }, + { + "label": "SexualCult", + "value": "230" + }, + { + "label": "SchemesAnd", + "value": "244" + }, + { + "label": "Sleeping", + "value": "275" + }, + { + "label": "Strength-b", + "value": "301" + }, + { + "label": "SiblingsNo", + "value": "340" + }, + { + "label": "SevenDeadl", + "value": "416" + }, + { + "label": "Sharp-tong", + "value": "457" + }, + { + "label": "StockholmS", + "value": "495" + }, + { + "label": "Student-Te", + "value": "513" + }, + { + "label": "Shapeshift", + "value": "525" + }, + { + "label": "Skyrim", + "value": "554" + }, + { + "label": "Sports", + "value": "586" + }, + { + "label": "SoundMagic", + "value": "591" + }, + { + "label": "Shounen-Ai", + "value": "596" + }, + { + "label": "Sci-Fantas", + "value": "606" + }, + { + "label": "slow-roman", + "value": "626" + }, + { + "label": "SecretRela", + "value": "658" + }, + { + "label": "SisterComp", + "value": "659" + }, + { + "label": "Shoujo-AiS", + "value": "661" + }, + { + "label": "SecretiveP", + "value": "708" + }, + { + "label": "sciencefic", + "value": "714" + }, + { + "label": "SexualAbus", + "value": "730" + }, + { + "label": "Spies", + "value": "731" + }, + { + "label": "StubbornPr", + "value": "736" + }, + { + "label": "SaveProtag", + "value": "772" + }, + { + "label": "Sibling&am", + "value": "774" + }, + { + "label": "SeeingThin", + "value": "776" + }, + { + "label": "SxFriends", + "value": "777" + }, + { + "label": "SharingABo", + "value": "783" + }, + { + "label": "SerialKill", + "value": "786" + }, + { + "label": "Singers", + "value": "832" + }, + { + "label": "SoftSci-fi", + "value": "840" + }, + { + "label": "Strategy", + "value": "843" + }, + { + "label": "Smartprota", + "value": "876" + }, + { + "label": "Strongfl", + "value": "878" + }, + { + "label": "Secretive", + "value": "890" + }, + { + "label": "SuperAbili", + "value": "891" + }, + { + "label": "Stepmom", + "value": "912" + }, + { + "label": "Secretary", + "value": "932" + }, + { + "label": "Studenttea", + "value": "940" + }, + { + "label": "Strongfema", + "value": "950" + }, + { + "label": "Sekai", + "value": "958" + }, + { + "label": "Signin", + "value": "977" + }, + { + "label": "Transmigra", + "value": "102" + }, + { + "label": "Tragedy", + "value": "794" + }, + { + "label": "TimeTravel", + "value": "389" + }, + { + "label": "Thestronga", + "value": "914" + }, + { + "label": "TimeSkip", + "value": "234" + }, + { + "label": "Technologi", + "value": "429" + }, + { + "label": "TragicPast", + "value": "44" + }, + { + "label": "Tsundere", + "value": "382" + }, + { + "label": "TimeManipu", + "value": "263" + }, + { + "label": "Thriller", + "value": "637" + }, + { + "label": "Teachers", + "value": "101" + }, + { + "label": "Twins", + "value": "506" + }, + { + "label": "Thieves", + "value": "514" + }, + { + "label": "Teamwork", + "value": "542" + }, + { + "label": "ThaiNovel", + "value": "815" + }, + { + "label": "TimeLoop", + "value": "696" + }, + { + "label": "Twisted", + "value": "906" + }, + { + "label": "TimeParado", + "value": "430" + }, + { + "label": "TimidProta", + "value": "473" + }, + { + "label": "Torture", + "value": "589" + }, + { + "label": "TwistedPer", + "value": "693" + }, + { + "label": "Threesome", + "value": "706" + }, + { + "label": "Teen", + "value": "857" + }, + { + "label": "Terrori", + "value": "884" + }, + { + "label": "Transporte", + "value": "103" + }, + { + "label": "Transporte", + "value": "278" + }, + { + "label": "Transporte", + "value": "375" + }, + { + "label": "Trap", + "value": "405" + }, + { + "label": "Transforma", + "value": "612" + }, + { + "label": "Tentacles", + "value": "666" + }, + { + "label": "Talesof D", + "value": "752" + }, + { + "label": "Transmigra", + "value": "764" + }, + { + "label": "Transplant", + "value": "768" + }, + { + "label": "Transporte", + "value": "784" + }, + { + "label": "TheAsteris", + "value": "826" + }, + { + "label": "TheGamer", + "value": "828" + }, + { + "label": "Trialmarri", + "value": "880" + }, + { + "label": "TheParanor", + "value": "887" + }, + { + "label": "TheMainCha", + "value": "896" + }, + { + "label": "TheDevil", + "value": "897" + }, + { + "label": "Urban", + "value": "522" + }, + { + "label": "Unprincipl", + "value": "923" + }, + { + "label": "UniqueWeap", + "value": "418" + }, + { + "label": "Undead", + "value": "965" + }, + { + "label": "UnluckyPro", + "value": "611" + }, + { + "label": "Unconditio", + "value": "445" + }, + { + "label": "Unreliable", + "value": "451" + }, + { + "label": "UglytoBeau", + "value": "550" + }, + { + "label": "Underestim", + "value": "79" + }, + { + "label": "UniqueCult", + "value": "81" + }, + { + "label": "UniqueWeap", + "value": "582" + }, + { + "label": "Villain", + "value": "861" + }, + { + "label": "VirtualRea", + "value": "433" + }, + { + "label": "Videogame", + "value": "925" + }, + { + "label": "Vampire", + "value": "602" + }, + { + "label": "Vampires", + "value": "323" + }, + { + "label": "Villainess", + "value": "680" + }, + { + "label": "VoiceActor", + "value": "446" + }, + { + "label": "Vietnamese", + "value": "820" + }, + { + "label": "Villainpro", + "value": "941" + }, + { + "label": "VideoGames", + "value": "973" + }, + { + "label": "WebNovel", + "value": "805" + }, + { + "label": "WeaktoStro", + "value": "83" + }, + { + "label": "Wars", + "value": "303" + }, + { + "label": "WealthyCha", + "value": "334" + }, + { + "label": "WorldTrave", + "value": "84" + }, + { + "label": "WorldHoppi", + "value": "504" + }, + { + "label": "Wizards", + "value": "571" + }, + { + "label": "Werewolf", + "value": "849" + }, + { + "label": "Werebeasts", + "value": "325" + }, + { + "label": "WeakProtag", + "value": "324" + }, + { + "label": "Witches", + "value": "570" + }, + { + "label": "WorldTree", + "value": "640" + }, + { + "label": "WeektoStro", + "value": "609" + }, + { + "label": "Wishes", + "value": "615" + }, + { + "label": "Writers", + "value": "662" + }, + { + "label": "WebnovelSp", + "value": "709" + }, + { + "label": "Warhammer4", + "value": "732" + }, + { + "label": "WarRecords", + "value": "745" + }, + { + "label": "Wuxia", + "value": "759" + }, + { + "label": "Worlds", + "value": "869" + }, + { + "label": "Weak-to-st", + "value": "959" + }, + { + "label": "Xianxia", + "value": "603" + }, + { + "label": "Xuanhuan", + "value": "773" + }, + { + "label": "Yandere", + "value": "402" + }, + { + "label": "Yuri", + "value": "929" + }, + { + "label": "Yaoi", + "value": "866" + }, + { + "label": "YoungerLov", + "value": "551" + }, + { + "label": "YoungerSis", + "value": "763" + }, + { + "label": "YoungerBro", + "value": "775" + }, + { + "label": "Zombies", + "value": "594" + }, + { + "label": "Zombie", + "value": "598" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readwn/filters/wuxiacity.json b/plugins/multisrc/readwn/filters/wuxiacity.json new file mode 100644 index 000000000..3c06aa0b7 --- /dev/null +++ b/plugins/multisrc/readwn/filters/wuxiacity.json @@ -0,0 +1,13468 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Sort By", + "value": "onclick", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "all", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre / Category", + "value": "", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Billionaire", + "value": "billionaire" + }, + { + "label": "CEO", + "value": "ceo" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Erciyuan", + "value": "erciyuan" + }, + { + "label": "Faloo", + "value": "faloo" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Farming", + "value": "farming" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Games", + "value": "games" + }, + { + "label": "Gay Romance", + "value": "gay-romance" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Historical Romance", + "value": "historical-romance" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Modern Life", + "value": "modern-life" + }, + { + "label": "Modern Romance", + "value": "modern-romance" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "Romantic", + "value": "romantic" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Two-dimensional", + "value": "two-dimensional" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Virtual Reality", + "value": "virtual-reality" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "tags": { + "type": "Picker", + "label": "Tags", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Action", + "value": "251" + }, + { + "label": "AncientChi", + "value": "94" + }, + { + "label": "Academy", + "value": "49" + }, + { + "label": "Apocalypse", + "value": "39" + }, + { + "label": "Adventure", + "value": "910" + }, + { + "label": "AncientTim", + "value": "50" + }, + { + "label": "Acting", + "value": "102" + }, + { + "label": "Alchemy", + "value": "22" + }, + { + "label": "ArrogantCh", + "value": "202" + }, + { + "label": "AdaptedtoM", + "value": "3" + }, + { + "label": "ArrangedMa", + "value": "116" + }, + { + "label": "AlternateW", + "value": "97" + }, + { + "label": "ArmyBuildi", + "value": "120" + }, + { + "label": "AdaptedtoM", + "value": "108" + }, + { + "label": "Aristocrac", + "value": "281" + }, + { + "label": "AgeProgres", + "value": "319" + }, + { + "label": "AntiheroPr", + "value": "234" + }, + { + "label": "Adventurer", + "value": "288" + }, + { + "label": "Amnesia", + "value": "495" + }, + { + "label": "Assassins", + "value": "23" + }, + { + "label": "Aliens", + "value": "95" + }, + { + "label": "AdaptedtoD", + "value": "93" + }, + { + "label": "AbsentPare", + "value": "249" + }, + { + "label": "AbusiveCha", + "value": "274" + }, + { + "label": "AntiHero", + "value": "819" + }, + { + "label": "Artifacts", + "value": "65" + }, + { + "label": "Army", + "value": "76" + }, + { + "label": "AbilitySte", + "value": "221" + }, + { + "label": "Appearance", + "value": "458" + }, + { + "label": "AbandonedC", + "value": "339" + }, + { + "label": "ApatheticP", + "value": "194" + }, + { + "label": "Accelerate", + "value": "236" + }, + { + "label": "AdoptedPro", + "value": "340" + }, + { + "label": "AgeRegress", + "value": "446" + }, + { + "label": "AdoptedChi", + "value": "404" + }, + { + "label": "ArtifactCr", + "value": "353" + }, + { + "label": "Angels", + "value": "31" + }, + { + "label": "AdaptedtoA", + "value": "17" + }, + { + "label": "AdaptedtoD", + "value": "486" + }, + { + "label": "Aggressive", + "value": "410" + }, + { + "label": "Adultery", + "value": "405" + }, + { + "label": "Aristocrat", + "value": "1193" + }, + { + "label": "Army-build", + "value": "1194" + }, + { + "label": "Archery", + "value": "112" + }, + { + "label": "ABO", + "value": "899" + }, + { + "label": "Artists", + "value": "422" + }, + { + "label": "Anime", + "value": "656" + }, + { + "label": "Affair", + "value": "605" + }, + { + "label": "AnimalRear", + "value": "432" + }, + { + "label": "Autism", + "value": "488" + }, + { + "label": "Anti-HeroL", + "value": "4" + }, + { + "label": "AwkwardPro", + "value": "707" + }, + { + "label": "AdaptedtoM", + "value": "810" + }, + { + "label": "advancedte", + "value": "1700" + }, + { + "label": "Anl", + "value": "748" + }, + { + "label": "AnotherWor", + "value": "864" + }, + { + "label": "AggresiveC", + "value": "1336" + }, + { + "label": "Anal", + "value": "575" + }, + { + "label": "Androids", + "value": "725" + }, + { + "label": "Abandoned", + "value": "1023" + }, + { + "label": "AdaptedtoM", + "value": "676" + }, + { + "label": "Ability", + "value": "1017" + }, + { + "label": "ArtifactsC", + "value": "253" + }, + { + "label": "ArmsDealer", + "value": "587" + }, + { + "label": "AdaptedtoV", + "value": "975" + }, + { + "label": "Adventurer", + "value": "1012" + }, + { + "label": "AdaptedtoG", + "value": "601" + }, + { + "label": "Adult", + "value": "1486" + }, + { + "label": "AgeGap", + "value": "1681" + }, + { + "label": "Alternativ", + "value": "3050" + }, + { + "label": "Almost", + "value": "990" + }, + { + "label": "Azeroth", + "value": "1745" + }, + { + "label": "AntiqueSho", + "value": "547" + }, + { + "label": "ApartmentL", + "value": "589" + }, + { + "label": "Assassin", + "value": "1098" + }, + { + "label": "alpha", + "value": "3098" + }, + { + "label": "Anti-Magic", + "value": "592" + }, + { + "label": "America", + "value": "3039" + }, + { + "label": "Award-winn", + "value": "1032" + }, + { + "label": "Actors", + "value": "1149" + }, + { + "label": "Ancient", + "value": "1582" + }, + { + "label": "Angel", + "value": "1625" + }, + { + "label": "AutomaticU", + "value": "1678" + }, + { + "label": "Abuse", + "value": "1707" + }, + { + "label": "Abilities", + "value": "1734" + }, + { + "label": "Almighty", + "value": "3254" + }, + { + "label": "Agedistrib", + "value": "3259" + }, + { + "label": "AI", + "value": "67" + }, + { + "label": "Astrologer", + "value": "779" + }, + { + "label": "Automatons", + "value": "794" + }, + { + "label": "AbusiveCha", + "value": "818" + }, + { + "label": "Adrogynous", + "value": "1152" + }, + { + "label": "Actress", + "value": "1563" + }, + { + "label": "animals", + "value": "1737" + }, + { + "label": "Age-gap", + "value": "1982" + }, + { + "label": "AmericanCo", + "value": "2939" + }, + { + "label": "Americas", + "value": "2943" + }, + { + "label": "Artificial", + "value": "43" + }, + { + "label": "AcasualPaw", + "value": "163" + }, + { + "label": "AncientChi", + "value": "232" + }, + { + "label": "Anti-socia", + "value": "233" + }, + { + "label": "Appearance", + "value": "411" + }, + { + "label": "AnimalChar", + "value": "497" + }, + { + "label": "Androgynou", + "value": "521" + }, + { + "label": "Average-lo", + "value": "524" + }, + { + "label": "Artist", + "value": "657" + }, + { + "label": "AmusementP", + "value": "765" + }, + { + "label": "ArtifactsB", + "value": "807" + }, + { + "label": "AverageLoo", + "value": "871" + }, + { + "label": "AgeDiffere", + "value": "908" + }, + { + "label": "ancientcit", + "value": "958" + }, + { + "label": "AttemptedM", + "value": "1070" + }, + { + "label": "ancienttim", + "value": "1096" + }, + { + "label": "AzurLane", + "value": "1111" + }, + { + "label": "Apocalypse", + "value": "1128" + }, + { + "label": "Anti-heroP", + "value": "1216" + }, + { + "label": "All-GirlsS", + "value": "1217" + }, + { + "label": "Anti-Hero", + "value": "1220" + }, + { + "label": "Appraisal", + "value": "1227" + }, + { + "label": "AI-chip", + "value": "1228" + }, + { + "label": "Apocalypse", + "value": "1279" + }, + { + "label": "AlternateH", + "value": "1303" + }, + { + "label": "AncientBus", + "value": "1304" + }, + { + "label": "Adopted", + "value": "1352" + }, + { + "label": "AutomaticU", + "value": "1364" + }, + { + "label": "Apprentice", + "value": "1426" + }, + { + "label": "ArmsTrade", + "value": "1437" + }, + { + "label": "anewworld", + "value": "1472" + }, + { + "label": "ancientset", + "value": "1502" + }, + { + "label": "Aggressive", + "value": "1513" + }, + { + "label": "AncientRea", + "value": "1515" + }, + { + "label": "Apocalypti", + "value": "1522" + }, + { + "label": "AcceptingD", + "value": "1543" + }, + { + "label": "Arknights", + "value": "1576" + }, + { + "label": "AnotherWor", + "value": "1586" + }, + { + "label": "AdvancedKn", + "value": "1600" + }, + { + "label": "AbandonedC", + "value": "1633" + }, + { + "label": "Aristrocac", + "value": "1634" + }, + { + "label": "Avatar&", + "value": "1674" + }, + { + "label": "Attractive", + "value": "1687" + }, + { + "label": "ACGN", + "value": "1692" + }, + { + "label": "AbsoluteDu", + "value": "1724" + }, + { + "label": "Alchemist", + "value": "1731" + }, + { + "label": "Abortion", + "value": "1739" + }, + { + "label": "Adoption", + "value": "1740" + }, + { + "label": "Animator", + "value": "1747" + }, + { + "label": "AncientWea", + "value": "1773" + }, + { + "label": "artificer", + "value": "1797" + }, + { + "label": "assasin", + "value": "1805" + }, + { + "label": "Aftertheso", + "value": "1859" + }, + { + "label": "atravellin", + "value": "1870" + }, + { + "label": "autumnautu", + "value": "1874" + }, + { + "label": "ahveryfish", + "value": "1917" + }, + { + "label": "agrass", + "value": "1926" + }, + { + "label": "AfricanEmi", + "value": "1931" + }, + { + "label": "AgeofGods", + "value": "1933" + }, + { + "label": "Apple", + "value": "2007" + }, + { + "label": "allenzhang", + "value": "2057" + }, + { + "label": "Authoroffa", + "value": "2059" + }, + { + "label": "Aaron&0", + "value": "2097" + }, + { + "label": "Ayanokoji", + "value": "2101" + }, + { + "label": "arayofsuns", + "value": "2160" + }, + { + "label": "animenewco", + "value": "2247" + }, + { + "label": "absolutely", + "value": "2294" + }, + { + "label": "anoldman", + "value": "2311" + }, + { + "label": "Auspicious", + "value": "2335" + }, + { + "label": "askTaichi", + "value": "2361" + }, + { + "label": "angryhouse", + "value": "2375" + }, + { + "label": "AllHeavens", + "value": "2387" + }, + { + "label": "Amagicpill", + "value": "2478" + }, + { + "label": "avigorous", + "value": "2513" + }, + { + "label": "Anautumnra", + "value": "2638" + }, + { + "label": "Archer", + "value": "2656" + }, + { + "label": "Alone", + "value": "2674" + }, + { + "label": "AZanpakut", + "value": "2693" + }, + { + "label": "Aliverday", + "value": "2716" + }, + { + "label": "Almightypl", + "value": "2842" + }, + { + "label": "AlmightyCo", + "value": "2845" + }, + { + "label": "AnlanInvin", + "value": "2905" + }, + { + "label": "AncientChi", + "value": "2932" + }, + { + "label": "AlterateHi", + "value": "2942" + }, + { + "label": "ArmsDealer", + "value": "2947" + }, + { + "label": "Anti-MC", + "value": "2956" + }, + { + "label": "Artificial", + "value": "2959" + }, + { + "label": "adventerer", + "value": "2992" + }, + { + "label": "ASOIAF", + "value": "2999" + }, + { + "label": "Assasins", + "value": "3014" + }, + { + "label": "armoredcit", + "value": "3023" + }, + { + "label": "Abyss", + "value": "3071" + }, + { + "label": "Animation", + "value": "3078" + }, + { + "label": "AnimationD", + "value": "3079" + }, + { + "label": "Avatar", + "value": "3100" + }, + { + "label": "A.I", + "value": "3128" + }, + { + "label": "ADeadBody", + "value": "3161" + }, + { + "label": "Anti-routi", + "value": "3252" + }, + { + "label": "Agent", + "value": "3278" + }, + { + "label": "Aesthetic", + "value": "3299" + }, + { + "label": "BeautifulF", + "value": "186" + }, + { + "label": "BusinessMa", + "value": "171" + }, + { + "label": "Betrayal", + "value": "25" + }, + { + "label": "BlackBelly", + "value": "170" + }, + { + "label": "BeastCompa", + "value": "280" + }, + { + "label": "BodyTemper", + "value": "61" + }, + { + "label": "Businessme", + "value": "201" + }, + { + "label": "Bloodlines", + "value": "114" + }, + { + "label": "Beasts", + "value": "135" + }, + { + "label": "BrokenEnga", + "value": "403" + }, + { + "label": "BickeringC", + "value": "467" + }, + { + "label": "Basketball", + "value": "228" + }, + { + "label": "Beastkin", + "value": "479" + }, + { + "label": "Bullying", + "value": "503" + }, + { + "label": "BattleComp", + "value": "284" + }, + { + "label": "BattleAcad", + "value": "380" + }, + { + "label": "Buddhism", + "value": "166" + }, + { + "label": "BrotherCom", + "value": "53" + }, + { + "label": "Bodyguards", + "value": "610" + }, + { + "label": "Blacksmith", + "value": "265" + }, + { + "label": "Brotherhoo", + "value": "426" + }, + { + "label": "Books", + "value": "433" + }, + { + "label": "Blackmail", + "value": "722" + }, + { + "label": "Bleach", + "value": "1168" + }, + { + "label": "BodySwap", + "value": "590" + }, + { + "label": "Business", + "value": "51" + }, + { + "label": "beautifulh", + "value": "434" + }, + { + "label": "Beasttamer", + "value": "1301" + }, + { + "label": "BDSM", + "value": "653" + }, + { + "label": "BlindProta", + "value": "720" + }, + { + "label": "BusinessEm", + "value": "658" + }, + { + "label": "Beasttamin", + "value": "3147" + }, + { + "label": "Brainwashi", + "value": "559" + }, + { + "label": "Bookworm", + "value": "577" + }, + { + "label": "Brave", + "value": "971" + }, + { + "label": "BasedonaMo", + "value": "369" + }, + { + "label": "blacktechn", + "value": "928" + }, + { + "label": "BigBroHasD", + "value": "154" + }, + { + "label": "Biochip", + "value": "406" + }, + { + "label": "Bloodpumpi", + "value": "3146" + }, + { + "label": "BloodManip", + "value": "485" + }, + { + "label": "Bestiality", + "value": "745" + }, + { + "label": "beauty", + "value": "1464" + }, + { + "label": "BlindDates", + "value": "607" + }, + { + "label": "Butlers", + "value": "708" + }, + { + "label": "Bulldozer", + "value": "1650" + }, + { + "label": "BasedonanA", + "value": "1093" + }, + { + "label": "BusinessMa", + "value": "1114" + }, + { + "label": "Bully", + "value": "54" + }, + { + "label": "Baseball", + "value": "550" + }, + { + "label": "Boxing", + "value": "737" + }, + { + "label": "BasedonaTV", + "value": "747" + }, + { + "label": "Beautifulg", + "value": "973" + }, + { + "label": "BeautifulP", + "value": "1204" + }, + { + "label": "Beast", + "value": "1300" + }, + { + "label": "book", + "value": "1462" + }, + { + "label": "Beautifull", + "value": "1552" + }, + { + "label": "buildingki", + "value": "1806" + }, + { + "label": "blooddemon", + "value": "2135" + }, + { + "label": "Bl", + "value": "3150" + }, + { + "label": "BisexualPr", + "value": "823" + }, + { + "label": "building", + "value": "929" + }, + { + "label": "bigharem", + "value": "931" + }, + { + "label": "BoysLove", + "value": "964" + }, + { + "label": "BraveandDe", + "value": "991" + }, + { + "label": "beastcompa", + "value": "1173" + }, + { + "label": "Bloodline", + "value": "1248" + }, + { + "label": "Blind", + "value": "1280" + }, + { + "label": "Beastmen", + "value": "1419" + }, + { + "label": "Billionair", + "value": "1494" + }, + { + "label": "BehindtheS", + "value": "1682" + }, + { + "label": "BungouStra", + "value": "1814" + }, + { + "label": "businessfl", + "value": "3262" + }, + { + "label": "Boss-Subor", + "value": "525" + }, + { + "label": "Black-bell", + "value": "891" + }, + { + "label": "BookTransm", + "value": "915" + }, + { + "label": "businessor", + "value": "920" + }, + { + "label": "Butnoconsp", + "value": "1002" + }, + { + "label": "Beautifula", + "value": "1043" + }, + { + "label": "BusinessDe", + "value": "1082" + }, + { + "label": "BlackBelly", + "value": "1097" + }, + { + "label": "BasedonaVi", + "value": "1130" + }, + { + "label": "BasedonaSo", + "value": "1163" + }, + { + "label": "BookWearer", + "value": "1198" + }, + { + "label": "Babies", + "value": "1269" + }, + { + "label": "Black-bell", + "value": "1324" + }, + { + "label": "BTTH", + "value": "1340" + }, + { + "label": "BattleThro", + "value": "1386" + }, + { + "label": "Breakup", + "value": "1395" + }, + { + "label": "BunguoStra", + "value": "1430" + }, + { + "label": "ben10", + "value": "1459" + }, + { + "label": "Bussiness", + "value": "1488" + }, + { + "label": "Beautifulf", + "value": "1493" + }, + { + "label": "BritishEmp", + "value": "1525" + }, + { + "label": "BehindtheS", + "value": "1541" + }, + { + "label": "Bussinesma", + "value": "1550" + }, + { + "label": "BuildKingd", + "value": "1575" + }, + { + "label": "Bloodborne", + "value": "1577" + }, + { + "label": "Blackbelli", + "value": "1592" + }, + { + "label": "Basket", + "value": "1642" + }, + { + "label": "Badassprot", + "value": "1738" + }, + { + "label": "beastman", + "value": "1743" + }, + { + "label": "Biomass", + "value": "1749" + }, + { + "label": "Blacklight", + "value": "1750" + }, + { + "label": "beautifulf", + "value": "1788" + }, + { + "label": "BeautifulF", + "value": "1790" + }, + { + "label": "bigpicture", + "value": "1833" + }, + { + "label": "BusinessRi", + "value": "1853" + }, + { + "label": "BloodofAni", + "value": "1861" + }, + { + "label": "Biscuits", + "value": "1879" + }, + { + "label": "Brownsugar", + "value": "1905" + }, + { + "label": "blackandwh", + "value": "1908" + }, + { + "label": "Bigplayers", + "value": "1916" + }, + { + "label": "bigwhitewh", + "value": "1950" + }, + { + "label": "Becomefamo", + "value": "1960" + }, + { + "label": "bloomingon", + "value": "1992" + }, + { + "label": "Boundlessf", + "value": "2024" + }, + { + "label": "ButterflyD", + "value": "2026" + }, + { + "label": "belovedbab", + "value": "2031" + }, + { + "label": "Bigdog", + "value": "2082" + }, + { + "label": "breezesilv", + "value": "2083" + }, + { + "label": "BuLofan", + "value": "2102" + }, + { + "label": "Brightmoon", + "value": "2115" + }, + { + "label": "Breeze", + "value": "2142" + }, + { + "label": "BraisedPai", + "value": "2172" + }, + { + "label": "BingtangHu", + "value": "2175" + }, + { + "label": "BookstoreS", + "value": "2177" + }, + { + "label": "Bearcat", + "value": "2179" + }, + { + "label": "blacksoil", + "value": "2181" + }, + { + "label": "BrotherChe", + "value": "2216" + }, + { + "label": "blueshirts", + "value": "2230" + }, + { + "label": "beatyourse", + "value": "2243" + }, + { + "label": "bluestone", + "value": "2245" + }, + { + "label": "bitefire", + "value": "2250" + }, + { + "label": "blackandim", + "value": "2254" + }, + { + "label": "BigSkeleto", + "value": "2268" + }, + { + "label": "BrotherZhu", + "value": "2270" + }, + { + "label": "Bearchildl", + "value": "2272" + }, + { + "label": "BloodMoonG", + "value": "2278" + }, + { + "label": "BarrenEmpe", + "value": "2300" + }, + { + "label": "breaktheke", + "value": "2304" + }, + { + "label": "beastprota", + "value": "2348" + }, + { + "label": "bigorangew", + "value": "2354" + }, + { + "label": "baldnessat", + "value": "2355" + }, + { + "label": "bigcitysma", + "value": "2406" + }, + { + "label": "BoXiaowen", + "value": "2432" + }, + { + "label": "baldman", + "value": "2433" + }, + { + "label": "Belltouche", + "value": "2440" + }, + { + "label": "BookDustSp", + "value": "2453" + }, + { + "label": "broalwaysg", + "value": "2532" + }, + { + "label": "Bodhicitta", + "value": "2562" + }, + { + "label": "beaming", + "value": "2565" + }, + { + "label": "Breakingth", + "value": "2624" + }, + { + "label": "Buildthewo", + "value": "2626" + }, + { + "label": "billionpeo", + "value": "2629" + }, + { + "label": "Bigcockcut", + "value": "2646" + }, + { + "label": "bigtent", + "value": "2666" + }, + { + "label": "boycold", + "value": "2683" + }, + { + "label": "becausesoh", + "value": "2705" + }, + { + "label": "Bringaknif", + "value": "2708" + }, + { + "label": "bearcocoa", + "value": "2713" + }, + { + "label": "bluesilksu", + "value": "2742" + }, + { + "label": "bighippo", + "value": "2749" + }, + { + "label": "beautifula", + "value": "2755" + }, + { + "label": "burnout", + "value": "2756" + }, + { + "label": "Burningmou", + "value": "2767" + }, + { + "label": "beggingfor", + "value": "2772" + }, + { + "label": "blackcatis", + "value": "2776" + }, + { + "label": "BlackDrago", + "value": "2789" + }, + { + "label": "Beansandgr", + "value": "2848" + }, + { + "label": "Boiled", + "value": "2855" + }, + { + "label": "blackandwh", + "value": "2859" + }, + { + "label": "BaiXiaowei", + "value": "2884" + }, + { + "label": "bewitching", + "value": "2890" + }, + { + "label": "balduncle", + "value": "2892" + }, + { + "label": "bluesilk", + "value": "2896" + }, + { + "label": "Boss", + "value": "2951" + }, + { + "label": "bookslikeu", + "value": "2962" + }, + { + "label": "Bandit", + "value": "2978" + }, + { + "label": "BuddhaofNi", + "value": "2989" + }, + { + "label": "blackice", + "value": "2990" + }, + { + "label": "BeautifulC", + "value": "3002" + }, + { + "label": "BearChild", + "value": "3013" + }, + { + "label": "BrotherInL", + "value": "3106" + }, + { + "label": "Blackening", + "value": "3108" + }, + { + "label": "BasedonaVi", + "value": "3119" + }, + { + "label": "bickeringl", + "value": "3139" + }, + { + "label": "Beatthemal", + "value": "3152" + }, + { + "label": "Beatthefem", + "value": "3153" + }, + { + "label": "Badboy", + "value": "3191" + }, + { + "label": "Bigshot", + "value": "3197" + }, + { + "label": "Baby", + "value": "3202" + }, + { + "label": "Biochemist", + "value": "3283" + }, + { + "label": "Bgfellow", + "value": "3298" + }, + { + "label": "Chinese", + "value": "923" + }, + { + "label": "Cultivatio", + "value": "46" + }, + { + "label": "CalmProtag", + "value": "227" + }, + { + "label": "CleverProt", + "value": "238" + }, + { + "label": "Cheats", + "value": "19" + }, + { + "label": "Celebritie", + "value": "18" + }, + { + "label": "CunningPro", + "value": "390" + }, + { + "label": "ComedicUnd", + "value": "299" + }, + { + "label": "Childcare", + "value": "268" + }, + { + "label": "ColdLoveIn", + "value": "344" + }, + { + "label": "CharacterG", + "value": "282" + }, + { + "label": "ChineseNov", + "value": "1708" + }, + { + "label": "ColdProtag", + "value": "229" + }, + { + "label": "comedy", + "value": "1191" + }, + { + "label": "CuteProtag", + "value": "367" + }, + { + "label": "CaringProt", + "value": "388" + }, + { + "label": "Cooking", + "value": "69" + }, + { + "label": "CuteChildr", + "value": "313" + }, + { + "label": "ConfidentP", + "value": "203" + }, + { + "label": "Cheat", + "value": "853" + }, + { + "label": "CuteStory", + "value": "368" + }, + { + "label": "CarefreePr", + "value": "418" + }, + { + "label": "CoupleGrow", + "value": "373" + }, + { + "label": "ChildhoodF", + "value": "364" + }, + { + "label": "Cross-dres", + "value": "250" + }, + { + "label": "CruelChara", + "value": "466" + }, + { + "label": "CollegeUni", + "value": "212" + }, + { + "label": "CautiousPr", + "value": "237" + }, + { + "label": "CharmingPr", + "value": "350" + }, + { + "label": "ChildProta", + "value": "363" + }, + { + "label": "ClingyLove", + "value": "307" + }, + { + "label": "ChildhoodL", + "value": "365" + }, + { + "label": "ChinesePre", + "value": "1356" + }, + { + "label": "Crime", + "value": "596" + }, + { + "label": "Crossdress", + "value": "1137" + }, + { + "label": "Crafting", + "value": "222" + }, + { + "label": "ChildAbuse", + "value": "412" + }, + { + "label": "Cohabitati", + "value": "470" + }, + { + "label": "ClanSectDe", + "value": "1368" + }, + { + "label": "ClanBuildi", + "value": "343" + }, + { + "label": "Contracts", + "value": "489" + }, + { + "label": "Chefs", + "value": "68" + }, + { + "label": "ClumsyLove", + "value": "469" + }, + { + "label": "Conquer", + "value": "3148" + }, + { + "label": "CosmicWars", + "value": "370" + }, + { + "label": "ChatGroup", + "value": "861" + }, + { + "label": "ChildhoodS", + "value": "217" + }, + { + "label": "Cannibalis", + "value": "289" + }, + { + "label": "Clones", + "value": "321" + }, + { + "label": "CharacterD", + "value": "384" + }, + { + "label": "ChildishPr", + "value": "419" + }, + { + "label": "CourtOffic", + "value": "522" + }, + { + "label": "Campus", + "value": "1728" + }, + { + "label": "ChatRooms", + "value": "73" + }, + { + "label": "Curses", + "value": "585" + }, + { + "label": "Criminals", + "value": "131" + }, + { + "label": "Crossover", + "value": "583" + }, + { + "label": "CardGames", + "value": "483" + }, + { + "label": "Corruption", + "value": "691" + }, + { + "label": "CEO", + "value": "1250" + }, + { + "label": "CowardlyPr", + "value": "490" + }, + { + "label": "comics", + "value": "1451" + }, + { + "label": "ChildhoodP", + "value": "366" + }, + { + "label": "Confinemen", + "value": "647" + }, + { + "label": "Cousins", + "value": "734" + }, + { + "label": "ChoiceSele", + "value": "1358" + }, + { + "label": "ciweimao", + "value": "3300" + }, + { + "label": "ComingofAg", + "value": "757" + }, + { + "label": "CampusLove", + "value": "468" + }, + { + "label": "CuriousPro", + "value": "617" + }, + { + "label": "Conditiona", + "value": "651" + }, + { + "label": "Crossing", + "value": "2941" + }, + { + "label": "Co-Workers", + "value": "729" + }, + { + "label": "Coma", + "value": "517" + }, + { + "label": "Clubs", + "value": "704" + }, + { + "label": "Counteratt", + "value": "1656" + }, + { + "label": "Celebrity", + "value": "911" + }, + { + "label": "Creation", + "value": "1298" + }, + { + "label": "clearthink", + "value": "3245" + }, + { + "label": "Chuunibyou", + "value": "813" + }, + { + "label": "CoolText", + "value": "1236" + }, + { + "label": "contempora", + "value": "1449" + }, + { + "label": "Chronology", + "value": "1485" + }, + { + "label": "College", + "value": "1491" + }, + { + "label": "city", + "value": "3236" + }, + { + "label": "Cityurban", + "value": "3293" + }, + { + "label": "Conflictin", + "value": "684" + }, + { + "label": "Cryostasis", + "value": "709" + }, + { + "label": "Creatures", + "value": "752" + }, + { + "label": "CloseComba", + "value": "1021" + }, + { + "label": "Cute", + "value": "1042" + }, + { + "label": "Childbirth", + "value": "1124" + }, + { + "label": "CautiousMc", + "value": "1143" + }, + { + "label": "Cnnilingus", + "value": "1311" + }, + { + "label": "Cards", + "value": "723" + }, + { + "label": "Cosplay", + "value": "781" + }, + { + "label": "ComedicUnd", + "value": "892" + }, + { + "label": "CommonerLi", + "value": "917" + }, + { + "label": "Card", + "value": "1138" + }, + { + "label": "ColdLoveIn", + "value": "1153" + }, + { + "label": "cunningfem", + "value": "1278" + }, + { + "label": "CosmicHorr", + "value": "1294" + }, + { + "label": "CrazyProta", + "value": "1295" + }, + { + "label": "CollegeorU", + "value": "1318" + }, + { + "label": "CampusLife", + "value": "1325" + }, + { + "label": "Cultivator", + "value": "1411" + }, + { + "label": "CuteChild", + "value": "1489" + }, + { + "label": "Cthulhu", + "value": "1578" + }, + { + "label": "Creator", + "value": "1585" + }, + { + "label": "Civilizati", + "value": "1649" + }, + { + "label": "cunningmc", + "value": "1801" + }, + { + "label": "ColdNightL", + "value": "2879" + }, + { + "label": "cunning", + "value": "2937" + }, + { + "label": "Cross", + "value": "3222" + }, + { + "label": "Chugoku", + "value": "3227" + }, + { + "label": "ClassicXia", + "value": "3231" + }, + { + "label": "Curse", + "value": "35" + }, + { + "label": "Celestials", + "value": "147" + }, + { + "label": "Charismati", + "value": "172" + }, + { + "label": "ComplexFam", + "value": "173" + }, + { + "label": "CleverProt", + "value": "594" + }, + { + "label": "Collection", + "value": "703" + }, + { + "label": "Commandand", + "value": "820" + }, + { + "label": "Chef", + "value": "834" + }, + { + "label": "Criminolog", + "value": "837" + }, + { + "label": "CalmMalePr", + "value": "839" + }, + { + "label": "ColdMaleLe", + "value": "879" + }, + { + "label": "CubRaising", + "value": "883" + }, + { + "label": "ContractLo", + "value": "921" + }, + { + "label": "Crossdress", + "value": "963" + }, + { + "label": "Collective", + "value": "1001" + }, + { + "label": "Cruelportr", + "value": "1045" + }, + { + "label": "CompanyMan", + "value": "1047" + }, + { + "label": "Cheerful", + "value": "1062" + }, + { + "label": "Constellat", + "value": "1069" + }, + { + "label": "Channel", + "value": "1072" + }, + { + "label": "ComplexFam", + "value": "1075" + }, + { + "label": "CoolMc", + "value": "1094" + }, + { + "label": "CareerOrie", + "value": "1104" + }, + { + "label": "Constructi", + "value": "1157" + }, + { + "label": "Complaint", + "value": "1179" + }, + { + "label": "CleverMc", + "value": "1188" + }, + { + "label": "ChildhoodS", + "value": "1200" + }, + { + "label": "Contract", + "value": "1210" + }, + { + "label": "Colonializ", + "value": "1245" + }, + { + "label": "chat-room", + "value": "1337" + }, + { + "label": "Companies", + "value": "1338" + }, + { + "label": "Complicate", + "value": "1343" + }, + { + "label": "Cluelessly", + "value": "1374" + }, + { + "label": "ChinaRefor", + "value": "1407" + }, + { + "label": "Church", + "value": "1409" + }, + { + "label": "Chaos", + "value": "1410" + }, + { + "label": "CluelessPr", + "value": "1415" + }, + { + "label": "ChuningMC", + "value": "1439" + }, + { + "label": "conquer", + "value": "1461" + }, + { + "label": "cultivatio", + "value": "1465" + }, + { + "label": "competitiv", + "value": "1468" + }, + { + "label": "comics", + "value": "1470" + }, + { + "label": "Capitalism", + "value": "1526" + }, + { + "label": "CivilServa", + "value": "1527" + }, + { + "label": "Conspirati", + "value": "1551" + }, + { + "label": "CuteProtag", + "value": "1553" + }, + { + "label": "CuteMaleLe", + "value": "1559" + }, + { + "label": "CaringMale", + "value": "1589" + }, + { + "label": "Comic", + "value": "1603" + }, + { + "label": "CunningPro", + "value": "1604" + }, + { + "label": "Club", + "value": "1619" + }, + { + "label": "Competitio", + "value": "1643" + }, + { + "label": "ChildhoodE", + "value": "1660" + }, + { + "label": "cluthullu", + "value": "1703" + }, + { + "label": "Chivalryof", + "value": "1723" + }, + { + "label": "ChineseAnc", + "value": "1735" + }, + { + "label": "ChinaNamba", + "value": "1748" + }, + { + "label": "ChenHegao", + "value": "1751" + }, + { + "label": "Contagonis", + "value": "1752" + }, + { + "label": "CutePet", + "value": "1774" + }, + { + "label": "chat", + "value": "1799" + }, + { + "label": "codegeass", + "value": "1813" + }, + { + "label": "Civilizati", + "value": "1816" + }, + { + "label": "coffeewith", + "value": "1844" + }, + { + "label": "Chirika", + "value": "1876" + }, + { + "label": "coverthesu", + "value": "1877" + }, + { + "label": "ColdStar&a", + "value": "1912" + }, + { + "label": "chaoticwor", + "value": "1919" + }, + { + "label": "catdaylist", + "value": "1947" + }, + { + "label": "CucumberHa", + "value": "1952" + }, + { + "label": "ChocolateI", + "value": "1962" + }, + { + "label": "cloudysky", + "value": "1967" + }, + { + "label": "CloudTop丨", + "value": "1987" + }, + { + "label": "CherryBlos", + "value": "2000" + }, + { + "label": "ChiDongdon", + "value": "2003" + }, + { + "label": "cuteshadow", + "value": "2008" + }, + { + "label": "Canolaflow", + "value": "2014" + }, + { + "label": "coyote", + "value": "2028" + }, + { + "label": "CloudSummi", + "value": "2036" + }, + { + "label": "ChanelNo.1", + "value": "2039" + }, + { + "label": "camera", + "value": "2045" + }, + { + "label": "canfly", + "value": "2055" + }, + { + "label": "catthatwan", + "value": "2056" + }, + { + "label": "coffeefatc", + "value": "2089" + }, + { + "label": "Cloudseest", + "value": "2092" + }, + { + "label": "Cloudtopfi", + "value": "2131" + }, + { + "label": "chef&03", + "value": "2136" + }, + { + "label": "Comeon", + "value": "2147" + }, + { + "label": "coldrivers", + "value": "2148" + }, + { + "label": "ChenTwelve", + "value": "2164" + }, + { + "label": "caviar", + "value": "2169" + }, + { + "label": "CloudTop丨", + "value": "2170" + }, + { + "label": "Catchtheca", + "value": "2180" + }, + { + "label": "cutegrapef", + "value": "2186" + }, + { + "label": "cartoonwil", + "value": "2205" + }, + { + "label": "ChefSurviv", + "value": "2215" + }, + { + "label": "Cloudtop丨", + "value": "2244" + }, + { + "label": "Crazyforam", + "value": "2261" + }, + { + "label": "Changeever", + "value": "2263" + }, + { + "label": "Canteendry", + "value": "2266" + }, + { + "label": "Comprehens", + "value": "2280" + }, + { + "label": "Catswithfi", + "value": "2310" + }, + { + "label": "CityGod", + "value": "2323" + }, + { + "label": "Cancat", + "value": "2333" + }, + { + "label": "catthousan", + "value": "2369" + }, + { + "label": "ChenChangf", + "value": "2374" + }, + { + "label": "Cicadasand", + "value": "2384" + }, + { + "label": "championge", + "value": "2405" + }, + { + "label": "Crazystory", + "value": "2444" + }, + { + "label": "Can&039", + "value": "2454" + }, + { + "label": "callthebea", + "value": "2462" + }, + { + "label": "CokeII", + "value": "2497" + }, + { + "label": "catgod", + "value": "2500" + }, + { + "label": "coldcolddo", + "value": "2510" + }, + { + "label": "Chosen12", + "value": "2585" + }, + { + "label": "coffeeinst", + "value": "2587" + }, + { + "label": "catloveson", + "value": "2596" + }, + { + "label": "civetcatat", + "value": "2620" + }, + { + "label": "catisrisin", + "value": "2625" + }, + { + "label": "CelestialC", + "value": "2668" + }, + { + "label": "catpowerfi", + "value": "2686" + }, + { + "label": "Can&039", + "value": "2689" + }, + { + "label": "Caicolorsh", + "value": "2700" + }, + { + "label": "CorpseFrag", + "value": "2726" + }, + { + "label": "codewordge", + "value": "2751" + }, + { + "label": "CarambolaJ", + "value": "2761" + }, + { + "label": "cockroache", + "value": "2792" + }, + { + "label": "city​​ya", + "value": "2800" + }, + { + "label": "Codeuntilt", + "value": "2809" + }, + { + "label": "cutepomelo", + "value": "2826" + }, + { + "label": "chasingthe", + "value": "2831" + }, + { + "label": "cloudmadeo", + "value": "2838" + }, + { + "label": "Cantaloupe", + "value": "2839" + }, + { + "label": "crookeddoo", + "value": "2844" + }, + { + "label": "cateatingp", + "value": "2850" + }, + { + "label": "Cupola", + "value": "2864" + }, + { + "label": "cornjuice", + "value": "2881" + }, + { + "label": "cutelovein", + "value": "2914" + }, + { + "label": "CampusRoma", + "value": "2919" + }, + { + "label": "ChainsawMa", + "value": "2929" + }, + { + "label": "Cruel", + "value": "2960" + }, + { + "label": "CangxueFei", + "value": "2968" + }, + { + "label": "Childhoodf", + "value": "2987" + }, + { + "label": "Cultivatio", + "value": "2993" + }, + { + "label": "Conspiracy", + "value": "3008" + }, + { + "label": "Calm", + "value": "3015" + }, + { + "label": "crimesolvi", + "value": "3034" + }, + { + "label": "curechildr", + "value": "3042" + }, + { + "label": "Cultivatio", + "value": "3043" + }, + { + "label": "child", + "value": "3077" + }, + { + "label": "CountrySid", + "value": "3103" + }, + { + "label": "Calmdown", + "value": "3118" + }, + { + "label": "CatchaGhos", + "value": "3163" + }, + { + "label": "ClassroomO", + "value": "3207" + }, + { + "label": "carpenter", + "value": "3214" + }, + { + "label": "cure", + "value": "3226" + }, + { + "label": "comprehens", + "value": "3232" + }, + { + "label": "CollegeStr", + "value": "3261" + }, + { + "label": "Comprehens", + "value": "3265" + }, + { + "label": "Chinesemed", + "value": "3274" + }, + { + "label": "Demons", + "value": "5" + }, + { + "label": "DevotedLov", + "value": "175" + }, + { + "label": "DotingLove", + "value": "230" + }, + { + "label": "Dragons", + "value": "24" + }, + { + "label": "Dark", + "value": "110" + }, + { + "label": "Depictions", + "value": "391" + }, + { + "label": "Doctors", + "value": "142" + }, + { + "label": "DenseProta", + "value": "398" + }, + { + "label": "DotingPare", + "value": "338" + }, + { + "label": "Dungeons", + "value": "32" + }, + { + "label": "DouluoDalu", + "value": "245" + }, + { + "label": "DemonLord", + "value": "40" + }, + { + "label": "DotingOlde", + "value": "337" + }, + { + "label": "DeathofLov", + "value": "462" + }, + { + "label": "Demi-Human", + "value": "70" + }, + { + "label": "Drama", + "value": "855" + }, + { + "label": "Daoism", + "value": "78" + }, + { + "label": "Death", + "value": "417" + }, + { + "label": "Divorce", + "value": "472" + }, + { + "label": "Disabiliti", + "value": "323" + }, + { + "label": "Discrimina", + "value": "475" + }, + { + "label": "Detectives", + "value": "633" + }, + { + "label": "DetectiveC", + "value": "962" + }, + { + "label": "Dwarfs", + "value": "449" + }, + { + "label": "DomesticAf", + "value": "459" + }, + { + "label": "DaoCompreh", + "value": "283" + }, + { + "label": "DragonBall", + "value": "1359" + }, + { + "label": "Dragon", + "value": "1399" + }, + { + "label": "Dreams", + "value": "541" + }, + { + "label": "Dwarves", + "value": "28" + }, + { + "label": "Destiny", + "value": "692" + }, + { + "label": "DaoCompani", + "value": "98" + }, + { + "label": "Depression", + "value": "308" + }, + { + "label": "DiscipleTr", + "value": "1370" + }, + { + "label": "DC", + "value": "840" + }, + { + "label": "Drugs", + "value": "413" + }, + { + "label": "Divination", + "value": "538" + }, + { + "label": "DungeonMas", + "value": "628" + }, + { + "label": "DarkFantas", + "value": "888" + }, + { + "label": "DemonKing", + "value": "970" + }, + { + "label": "Doomsday", + "value": "1129" + }, + { + "label": "Demon", + "value": "1276" + }, + { + "label": "Detective", + "value": "903" + }, + { + "label": "Devil", + "value": "952" + }, + { + "label": "Delinquent", + "value": "552" + }, + { + "label": "DemonSlaye", + "value": "1169" + }, + { + "label": "Doctor", + "value": "1282" + }, + { + "label": "DollsPuppe", + "value": "544" + }, + { + "label": "Daily", + "value": "969" + }, + { + "label": "DivineProt", + "value": "375" + }, + { + "label": "DeadProtag", + "value": "644" + }, + { + "label": "Debts", + "value": "710" + }, + { + "label": "Disfigurem", + "value": "399" + }, + { + "label": "DishonestP", + "value": "505" + }, + { + "label": "DragonSlay", + "value": "624" + }, + { + "label": "Dystopia", + "value": "681" + }, + { + "label": "Deepl", + "value": "941" + }, + { + "label": "Devils", + "value": "92" + }, + { + "label": "Dancers", + "value": "509" + }, + { + "label": "Danmei", + "value": "599" + }, + { + "label": "Druids", + "value": "636" + }, + { + "label": "Dinosaurs", + "value": "1167" + }, + { + "label": "Director", + "value": "1238" + }, + { + "label": "dotinglove", + "value": "1423" + }, + { + "label": "Dramatic", + "value": "3183" + }, + { + "label": "Doujinshi", + "value": "3242" + }, + { + "label": "Dream", + "value": "615" + }, + { + "label": "Delusions", + "value": "815" + }, + { + "label": "DotingPare", + "value": "835" + }, + { + "label": "Doupo", + "value": "842" + }, + { + "label": "DeepLTrans", + "value": "1421" + }, + { + "label": "Dynasty", + "value": "3009" + }, + { + "label": "DarkDeatho", + "value": "649" + }, + { + "label": "DoupoBTTH", + "value": "831" + }, + { + "label": "Digimon", + "value": "1809" + }, + { + "label": "Demondomai", + "value": "2450" + }, + { + "label": "dreamblizz", + "value": "2875" + }, + { + "label": "Dining", + "value": "3264" + }, + { + "label": "Determined", + "value": "174" + }, + { + "label": "Divination", + "value": "294" + }, + { + "label": "DemonicCul", + "value": "322" + }, + { + "label": "DifferentS", + "value": "374" + }, + { + "label": "Distrustfu", + "value": "685" + }, + { + "label": "Differenta", + "value": "989" + }, + { + "label": "DifferentW", + "value": "1041" + }, + { + "label": "Disqualifi", + "value": "1063" + }, + { + "label": "DumbProtag", + "value": "1117" + }, + { + "label": "Diplomacy", + "value": "1148" + }, + { + "label": "Determined", + "value": "1189" + }, + { + "label": "DoubleLife", + "value": "1232" + }, + { + "label": "Depictions", + "value": "1267" + }, + { + "label": "DoubleRebi", + "value": "1273" + }, + { + "label": "Doujin", + "value": "1345" + }, + { + "label": "dungeon", + "value": "1404" + }, + { + "label": "DarkPower", + "value": "1413" + }, + { + "label": "differentw", + "value": "1417" + }, + { + "label": "dotingfami", + "value": "1422" + }, + { + "label": "DiscipleLo", + "value": "1427" + }, + { + "label": "dotinghusb", + "value": "1445" + }, + { + "label": "Diplomats", + "value": "1528" + }, + { + "label": "Dominator", + "value": "1539" + }, + { + "label": "DestinedLo", + "value": "1567" + }, + { + "label": "Doomdays", + "value": "1568" + }, + { + "label": "Dwarf", + "value": "1595" + }, + { + "label": "Disobedien", + "value": "1668" + }, + { + "label": "DanMachi", + "value": "1675" + }, + { + "label": "DotingSibl", + "value": "1704" + }, + { + "label": "DisabledPr", + "value": "1736" + }, + { + "label": "DoingBusin", + "value": "1759" + }, + { + "label": "Devotedlov", + "value": "1778" + }, + { + "label": "Dog", + "value": "1791" + }, + { + "label": "DevotedCou", + "value": "1817" + }, + { + "label": "dreamleave", + "value": "1838" + }, + { + "label": "divinesign", + "value": "1845" + }, + { + "label": "darkpirate", + "value": "1855" + }, + { + "label": "darknight", + "value": "1920" + }, + { + "label": "dragracing", + "value": "1975" + }, + { + "label": "DatangDaqi", + "value": "1977" + }, + { + "label": "dancetofig", + "value": "1996" + }, + { + "label": "Decadeligh", + "value": "2001" + }, + { + "label": "don&039", + "value": "2010" + }, + { + "label": "deepbluese", + "value": "2016" + }, + { + "label": "DatangErwu", + "value": "2030" + }, + { + "label": "Datangsupe", + "value": "2042" + }, + { + "label": "DragonPala", + "value": "2043" + }, + { + "label": "digitalold", + "value": "2062" + }, + { + "label": "DouTuKing", + "value": "2099" + }, + { + "label": "don&039", + "value": "2105" + }, + { + "label": "daughterco", + "value": "2121" + }, + { + "label": "Dreamofthe", + "value": "2190" + }, + { + "label": "DamingYong", + "value": "2202" + }, + { + "label": "DaoyanShen", + "value": "2226" + }, + { + "label": "DemonInvas", + "value": "2260" + }, + { + "label": "DaqingXiao", + "value": "2276" + }, + { + "label": "Dollsister", + "value": "2292" + }, + { + "label": "DragonBall", + "value": "2330" + }, + { + "label": "doyoueator", + "value": "2334" + }, + { + "label": "Devilveget", + "value": "2336" + }, + { + "label": "Destroyerf", + "value": "2350" + }, + { + "label": "deadfatfas", + "value": "2360" + }, + { + "label": "Dahunjun", + "value": "2385" + }, + { + "label": "Desperatel", + "value": "2392" + }, + { + "label": "DatangDaqi", + "value": "2403" + }, + { + "label": "dragon-eat", + "value": "2436" + }, + { + "label": "dreamintot", + "value": "2446" + }, + { + "label": "Dashuaihen", + "value": "2464" + }, + { + "label": "Daddywants", + "value": "2470" + }, + { + "label": "dogeggsold", + "value": "2514" + }, + { + "label": "dreamcatch", + "value": "2530" + }, + { + "label": "DivineBook", + "value": "2531" + }, + { + "label": "doyouwantc", + "value": "2546" + }, + { + "label": "Don&039", + "value": "2549" + }, + { + "label": "dagougou", + "value": "2569" + }, + { + "label": "DriftwoodD", + "value": "2571" + }, + { + "label": "Dikabenka", + "value": "2603" + }, + { + "label": "Daybyday", + "value": "2604" + }, + { + "label": "Diga", + "value": "2617" + }, + { + "label": "Donotbecon", + "value": "2622" + }, + { + "label": "Donotforge", + "value": "2635" + }, + { + "label": "digthreefe", + "value": "2662" + }, + { + "label": "Doomsdaywa", + "value": "2694" + }, + { + "label": "DoctorData", + "value": "2720" + }, + { + "label": "DragonandL", + "value": "2735" + }, + { + "label": "dirtylittl", + "value": "2802" + }, + { + "label": "Drunklifed", + "value": "2897" + }, + { + "label": "Datangpota", + "value": "2906" + }, + { + "label": "dimensiona", + "value": "2918" + }, + { + "label": "Doraemon", + "value": "2930" + }, + { + "label": "Domineerin", + "value": "2952" + }, + { + "label": "Douluo", + "value": "2957" + }, + { + "label": "Decisive", + "value": "2976" + }, + { + "label": "DemonPower", + "value": "3025" + }, + { + "label": "DragonPowe", + "value": "3026" + }, + { + "label": "DecisiveMc", + "value": "3093" + }, + { + "label": "disability", + "value": "3130" + }, + { + "label": "Dailylife", + "value": "3239" + }, + { + "label": "doctorstre", + "value": "3260" + }, + { + "label": "Diablo", + "value": "3284" + }, + { + "label": "Evolution", + "value": "52" + }, + { + "label": "EarlyRoman", + "value": "104" + }, + { + "label": "Elves", + "value": "6" + }, + { + "label": "Entertainm", + "value": "146" + }, + { + "label": "EvilProtag", + "value": "471" + }, + { + "label": "Episodic", + "value": "608" + }, + { + "label": "EnemiesBec", + "value": "309" + }, + { + "label": "ElementalM", + "value": "213" + }, + { + "label": "EvilGods", + "value": "381" + }, + { + "label": "Entertainm", + "value": "869" + }, + { + "label": "e-Sports", + "value": "335" + }, + { + "label": "EyePowers", + "value": "324" + }, + { + "label": "EuropeanAm", + "value": "621" + }, + { + "label": "Exorcism", + "value": "487" + }, + { + "label": "Empires", + "value": "133" + }, + { + "label": "EasyGoingL", + "value": "300" + }, + { + "label": "EideticMem", + "value": "423" + }, + { + "label": "Engagement", + "value": "447" + }, + { + "label": "EnemiesBec", + "value": "476" + }, + { + "label": "Economics", + "value": "316" + }, + { + "label": "EvilOrgani", + "value": "356" + }, + { + "label": "Eunuch", + "value": "409" + }, + { + "label": "EasternSet", + "value": "1078" + }, + { + "label": "Ecchi", + "value": "3200" + }, + { + "label": "EvilReligi", + "value": "392" + }, + { + "label": "ESNGrandPr", + "value": "999" + }, + { + "label": "Egoist", + "value": "3201" + }, + { + "label": "EarthInvas", + "value": "304" + }, + { + "label": "Engineer", + "value": "306" + }, + { + "label": "easternfan", + "value": "396" + }, + { + "label": "Exhaustion", + "value": "3061" + }, + { + "label": "Experience", + "value": "1387" + }, + { + "label": "enemiestol", + "value": "1812" + }, + { + "label": "Europe", + "value": "3102" + }, + { + "label": "Eschatolog", + "value": "3221" + }, + { + "label": "Enlightenm", + "value": "342" + }, + { + "label": "EvilGod", + "value": "1102" + }, + { + "label": "Elf", + "value": "1348" + }, + { + "label": "EuropeanAm", + "value": "914" + }, + { + "label": "Empress", + "value": "966" + }, + { + "label": "EvilMistre", + "value": "982" + }, + { + "label": "Exhibition", + "value": "1312" + }, + { + "label": "EnemytoLov", + "value": "1661" + }, + { + "label": "Evergrande", + "value": "2769" + }, + { + "label": "eincarnate", + "value": "75" + }, + { + "label": "Evil", + "value": "165" + }, + { + "label": "Emotionall", + "value": "188" + }, + { + "label": "Editors", + "value": "799" + }, + { + "label": "Entertainm", + "value": "894" + }, + { + "label": "Everyoneli", + "value": "1025" + }, + { + "label": "EnemytoLov", + "value": "1201" + }, + { + "label": "Entertaime", + "value": "1211" + }, + { + "label": "Exorcist", + "value": "1239" + }, + { + "label": "Empire", + "value": "1246" + }, + { + "label": "EvilCharac", + "value": "1296" + }, + { + "label": "Elite", + "value": "1320" + }, + { + "label": "eyepower", + "value": "1432" + }, + { + "label": "EvilOrgani", + "value": "1435" + }, + { + "label": "evolution", + "value": "1452" + }, + { + "label": "Evil-prota", + "value": "1516" + }, + { + "label": "Emperialpo", + "value": "1549" + }, + { + "label": "Ex-girlfri", + "value": "1570" + }, + { + "label": "Easygoingp", + "value": "1593" + }, + { + "label": "Eccentricp", + "value": "1632" + }, + { + "label": "Extraordin", + "value": "1639" + }, + { + "label": "EatingBroa", + "value": "1669" + }, + { + "label": "entertainm", + "value": "1742" + }, + { + "label": "EvilSprits", + "value": "1755" + }, + { + "label": "exes", + "value": "1794" + }, + { + "label": "electricia", + "value": "1811" + }, + { + "label": "EunuchJinr", + "value": "1865" + }, + { + "label": "Elfcold", + "value": "1941" + }, + { + "label": "EmperorYao", + "value": "1978" + }, + { + "label": "Eggpie", + "value": "2223" + }, + { + "label": "Extremelyi", + "value": "2283" + }, + { + "label": "Evergrande", + "value": "2391" + }, + { + "label": "emptymonol", + "value": "2448" + }, + { + "label": "Entertaini", + "value": "2496" + }, + { + "label": "eternityor", + "value": "2502" + }, + { + "label": "EmperorCha", + "value": "2547" + }, + { + "label": "EndoftheWo", + "value": "2556" + }, + { + "label": "everydayfi", + "value": "2563" + }, + { + "label": "entertainm", + "value": "2651" + }, + { + "label": "electricmo", + "value": "2687" + }, + { + "label": "engageinba", + "value": "2731" + }, + { + "label": "everlastin", + "value": "2736" + }, + { + "label": "Erwazi", + "value": "2841" + }, + { + "label": "entertainm", + "value": "2843" + }, + { + "label": "Eggplantan", + "value": "2852" + }, + { + "label": "Eighteence", + "value": "2964" + }, + { + "label": "eartwarmin", + "value": "3027" + }, + { + "label": "Emperor", + "value": "3037" + }, + { + "label": "Emotional", + "value": "3082" + }, + { + "label": "elemental", + "value": "3111" + }, + { + "label": "empressfem", + "value": "3132" + }, + { + "label": "EvilSpirit", + "value": "3162" + }, + { + "label": "Ex", + "value": "3171" + }, + { + "label": "Esper", + "value": "3216" + }, + { + "label": "evolutiona", + "value": "3241" + }, + { + "label": "Elixirs", + "value": "3255" + }, + { + "label": "Engage", + "value": "3258" + }, + { + "label": "Faloo", + "value": "1040" + }, + { + "label": "FemaleProt", + "value": "55" + }, + { + "label": "Fan-fictio", + "value": "100" + }, + { + "label": "Fantasy", + "value": "397" + }, + { + "label": "Fanfiction", + "value": "235" + }, + { + "label": "FantasyWor", + "value": "189" + }, + { + "label": "fanqienove", + "value": "3129" + }, + { + "label": "FastCultiv", + "value": "239" + }, + { + "label": "Farming", + "value": "96" + }, + { + "label": "Family", + "value": "314" + }, + { + "label": "FamilialLo", + "value": "214" + }, + { + "label": "Futuristic", + "value": "241" + }, + { + "label": "FamilyConf", + "value": "176" + }, + { + "label": "FirstLove", + "value": "454" + }, + { + "label": "Friendship", + "value": "501" + }, + { + "label": "FamousProt", + "value": "269" + }, + { + "label": "FastLearne", + "value": "240" + }, + { + "label": "Football", + "value": "134" + }, + { + "label": "FantasyCre", + "value": "516" + }, + { + "label": "FatedLover", + "value": "437" + }, + { + "label": "FantasyMag", + "value": "382" + }, + { + "label": "FaceSlappi", + "value": "880" + }, + { + "label": "Firearms", + "value": "137" + }, + { + "label": "FamilyBusi", + "value": "401" + }, + { + "label": "ForcedMarr", + "value": "609" + }, + { + "label": "FairyTail", + "value": "940" + }, + { + "label": "FattoFit", + "value": "480" + }, + { + "label": "First-time", + "value": "850" + }, + { + "label": "Fanfic", + "value": "1240" + }, + { + "label": "Fellatio", + "value": "204" + }, + { + "label": "FutureCivi", + "value": "542" + }, + { + "label": "FemaleMast", + "value": "395" + }, + { + "label": "FoxSpirits", + "value": "325" + }, + { + "label": "FoodWars!", + "value": "1369" + }, + { + "label": "FearlessPr", + "value": "402" + }, + { + "label": "FamousPare", + "value": "464" + }, + { + "label": "FengShui", + "value": "532" + }, + { + "label": "Fllatio", + "value": "744" + }, + { + "label": "Fairies", + "value": "58" + }, + { + "label": "Finance", + "value": "2935" + }, + { + "label": "Fastpaced", + "value": "3182" + }, + { + "label": "Flashbacks", + "value": "616" + }, + { + "label": "FatProtago", + "value": "349" + }, + { + "label": "FemaletoMa", + "value": "696" + }, + { + "label": "FemaleLead", + "value": "1305" + }, + { + "label": "Filipino", + "value": "1713" + }, + { + "label": "Faceslap", + "value": "1657" + }, + { + "label": "Future", + "value": "1741" + }, + { + "label": "Fujoshi", + "value": "576" + }, + { + "label": "FallenNobi", + "value": "716" + }, + { + "label": "FilipinoNo", + "value": "1712" + }, + { + "label": "futureworl", + "value": "2949" + }, + { + "label": "Funny", + "value": "3089" + }, + { + "label": "Food", + "value": "824" + }, + { + "label": "FormerHero", + "value": "711" + }, + { + "label": "FusionFant", + "value": "1064" + }, + { + "label": "FleetBattl", + "value": "626" + }, + { + "label": "FriendsBec", + "value": "650" + }, + { + "label": "Folklore", + "value": "673" + }, + { + "label": "Fanaticism", + "value": "772" + }, + { + "label": "FallenAnge", + "value": "809" + }, + { + "label": "Futuristic", + "value": "1016" + }, + { + "label": "Fishing", + "value": "1176" + }, + { + "label": "Fusión", + "value": "1326" + }, + { + "label": "Fatedlove", + "value": "3157" + }, + { + "label": "Formations", + "value": "150" + }, + { + "label": "Familiars", + "value": "1057" + }, + { + "label": "FamilyBuil", + "value": "1115" + }, + { + "label": "FarmingTex", + "value": "1264" + }, + { + "label": "Femaleprot", + "value": "1272" + }, + { + "label": "FastGrowth", + "value": "1315" + }, + { + "label": "FemalePres", + "value": "1495" + }, + { + "label": "Farm", + "value": "1694" + }, + { + "label": "famouscoup", + "value": "1702" + }, + { + "label": "FengziXiao", + "value": "2109" + }, + { + "label": "ForgetfulP", + "value": "420" + }, + { + "label": "First-time", + "value": "481" + }, + { + "label": "Futanari", + "value": "496" + }, + { + "label": "FemaleMast", + "value": "595" + }, + { + "label": "Forcedinto", + "value": "619" + }, + { + "label": "ForcedLivi", + "value": "683" + }, + { + "label": "Friction", + "value": "901" + }, + { + "label": "FaketoReal", + "value": "922" + }, + { + "label": "Fighting", + "value": "1003" + }, + { + "label": "FemaleProt", + "value": "1051" + }, + { + "label": "FemalesPro", + "value": "1224" + }, + { + "label": "FamillialL", + "value": "1247" + }, + { + "label": "FutureCivi", + "value": "1299" + }, + { + "label": "FourthDisa", + "value": "1328" + }, + { + "label": "FemaleMC", + "value": "1440" + }, + { + "label": "fatedxd", + "value": "1453" + }, + { + "label": "fanfic", + "value": "1471" + }, + { + "label": "FastWearin", + "value": "1503" + }, + { + "label": "FemaleSpie", + "value": "1529" + }, + { + "label": "France", + "value": "1533" + }, + { + "label": "FanFicton", + "value": "1581" + }, + { + "label": "FateSeries", + "value": "1621" + }, + { + "label": "FemaleFigh", + "value": "1622" + }, + { + "label": "familylife", + "value": "1627" + }, + { + "label": "FamilyLove", + "value": "1727" + }, + { + "label": "Fantasyfut", + "value": "1795" + }, + { + "label": "Firethief", + "value": "1842" + }, + { + "label": "Fairy丨Pin", + "value": "1864" + }, + { + "label": "foxlisteni", + "value": "1867" + }, + { + "label": "flamingfla", + "value": "1884" + }, + { + "label": "Friday", + "value": "1939" + }, + { + "label": "Famousdete", + "value": "1957" + }, + { + "label": "FerrariEnz", + "value": "1970" + }, + { + "label": "FallingRai", + "value": "1985" + }, + { + "label": "FeiLuEdiso", + "value": "1986" + }, + { + "label": "FairySword", + "value": "2005" + }, + { + "label": "firstperso", + "value": "2017" + }, + { + "label": "Fireinthes", + "value": "2079" + }, + { + "label": "fatmanoffa", + "value": "2098" + }, + { + "label": "fishfishda", + "value": "2119" + }, + { + "label": "Followthew", + "value": "2133" + }, + { + "label": "Fallenleav", + "value": "2146" + }, + { + "label": "Favoritebl", + "value": "2149" + }, + { + "label": "fierce", + "value": "2165" + }, + { + "label": "forest", + "value": "2174" + }, + { + "label": "flyingfish", + "value": "2183" + }, + { + "label": "fullmeal", + "value": "2212" + }, + { + "label": "Forgiveyou", + "value": "2249" + }, + { + "label": "Fahaiunder", + "value": "2286" + }, + { + "label": "FantaCola", + "value": "2306" + }, + { + "label": "FanJiu", + "value": "2316" + }, + { + "label": "FlyingLuTi", + "value": "2340" + }, + { + "label": "furioussna", + "value": "2380" + }, + { + "label": "flyingsqui", + "value": "2408" + }, + { + "label": "FangQingya", + "value": "2410" + }, + { + "label": "FoxdemonXi", + "value": "2415" + }, + { + "label": "FireWinged", + "value": "2421" + }, + { + "label": "Fengqing", + "value": "2463" + }, + { + "label": "FightingCo", + "value": "2490" + }, + { + "label": "FifthEmper", + "value": "2493" + }, + { + "label": "Fourkeys", + "value": "2494" + }, + { + "label": "fishandraf", + "value": "2512" + }, + { + "label": "fairygirlf", + "value": "2523" + }, + { + "label": "Fantasybos", + "value": "2552" + }, + { + "label": "flyinglitt", + "value": "2581" + }, + { + "label": "firstgreen", + "value": "2595" + }, + { + "label": "flyingcow", + "value": "2598" + }, + { + "label": "Floatingli", + "value": "2614" + }, + { + "label": "fakegod", + "value": "2616" + }, + { + "label": "fisheatpan", + "value": "2630" + }, + { + "label": "FatDiddy", + "value": "2649" + }, + { + "label": "fireonfire", + "value": "2657" + }, + { + "label": "flyinthelo", + "value": "2675" + }, + { + "label": "FahaiInvin", + "value": "2722" + }, + { + "label": "Flyingwhit", + "value": "2738" + }, + { + "label": "Faucet", + "value": "2770" + }, + { + "label": "flyingshar", + "value": "2777" + }, + { + "label": "fanofstar", + "value": "2793" + }, + { + "label": "fishinflam", + "value": "2817" + }, + { + "label": "FallenWing", + "value": "2822" + }, + { + "label": "Favoriteco", + "value": "2823" + }, + { + "label": "fishswimmi", + "value": "2834" + }, + { + "label": "Fifi&03", + "value": "2868" + }, + { + "label": "Fishheadis", + "value": "2870" + }, + { + "label": "flowersoft", + "value": "2872" + }, + { + "label": "fallintoth", + "value": "2874" + }, + { + "label": "formworksk", + "value": "2882" + }, + { + "label": "FemaleProt", + "value": "2953" + }, + { + "label": "Fatestayni", + "value": "2995" + }, + { + "label": "Fairy", + "value": "3016" + }, + { + "label": "Fullcolor", + "value": "3053" + }, + { + "label": "FemaleEmpe", + "value": "3073" + }, + { + "label": "Formation", + "value": "3076" + }, + { + "label": "FantasyCre", + "value": "3101" + }, + { + "label": "FemalePart", + "value": "3107" + }, + { + "label": "fasttravel", + "value": "3109" + }, + { + "label": "Fightforhe", + "value": "3116" + }, + { + "label": "futuredyst", + "value": "3134" + }, + { + "label": "Fantasyrom", + "value": "3184" + }, + { + "label": "Forbiddenl", + "value": "3190" + }, + { + "label": "Friendstol", + "value": "3192" + }, + { + "label": "Fastpace", + "value": "3204" + }, + { + "label": "Fiction", + "value": "3213" + }, + { + "label": "Fan", + "value": "3230" + }, + { + "label": "Focusonexp", + "value": "3249" + }, + { + "label": "Farmer", + "value": "3267" + }, + { + "label": "Furutake", + "value": "3275" + }, + { + "label": "Foreigncou", + "value": "3277" + }, + { + "label": "fqloo", + "value": "3303" + }, + { + "label": "GameElemen", + "value": "8" + }, + { + "label": "GeniusProt", + "value": "252" + }, + { + "label": "Ghosts", + "value": "82" + }, + { + "label": "Gods", + "value": "242" + }, + { + "label": "Gamers", + "value": "190" + }, + { + "label": "GodlyPower", + "value": "383" + }, + { + "label": "GodProtago", + "value": "191" + }, + { + "label": "genius", + "value": "1467" + }, + { + "label": "Gore", + "value": "10" + }, + { + "label": "GatetoAnot", + "value": "326" + }, + { + "label": "GameRankin", + "value": "443" + }, + { + "label": "GeneticMod", + "value": "285" + }, + { + "label": "Generals", + "value": "513" + }, + { + "label": "Guilds", + "value": "11" + }, + { + "label": "Goddesses", + "value": "444" + }, + { + "label": "Game", + "value": "827" + }, + { + "label": "Gangs", + "value": "143" + }, + { + "label": "GeneModifi", + "value": "1367" + }, + { + "label": "Gunfighter", + "value": "518" + }, + { + "label": "Genderbend", + "value": "1054" + }, + { + "label": "Goblins", + "value": "9" + }, + { + "label": "GamingE-Sp", + "value": "336" + }, + { + "label": "GameElemen", + "value": "858" + }, + { + "label": "Growth", + "value": "1024" + }, + { + "label": "Grinding", + "value": "625" + }, + { + "label": "God", + "value": "1349" + }, + { + "label": "Gangsters", + "value": "90" + }, + { + "label": "Gambling", + "value": "89" + }, + { + "label": "GuardianRe", + "value": "614" + }, + { + "label": "Grimdark", + "value": "60" + }, + { + "label": "GoldenFing", + "value": "1083" + }, + { + "label": "Gaming", + "value": "1178" + }, + { + "label": "GameRangki", + "value": "1292" + }, + { + "label": "Golems", + "value": "156" + }, + { + "label": "Ghost", + "value": "660" + }, + { + "label": "Genies", + "value": "726" + }, + { + "label": "GenshinImp", + "value": "1372" + }, + { + "label": "gamealien", + "value": "3233" + }, + { + "label": "geniusflow", + "value": "3244" + }, + { + "label": "gameworld", + "value": "1266" + }, + { + "label": "GameOnline", + "value": "1487" + }, + { + "label": "Gundam", + "value": "1784" + }, + { + "label": "GameofThro", + "value": "2998" + }, + { + "label": "Gettingbac", + "value": "3170" + }, + { + "label": "Giants", + "value": "164" + }, + { + "label": "Glasses-we", + "value": "506" + }, + { + "label": "God-humanR", + "value": "618" + }, + { + "label": "Glasses-we", + "value": "690" + }, + { + "label": "Genderless", + "value": "697" + }, + { + "label": "Gamedesign", + "value": "937" + }, + { + "label": "Growthsyst", + "value": "1039" + }, + { + "label": "GreekMytho", + "value": "1067" + }, + { + "label": "GodlyProta", + "value": "1085" + }, + { + "label": "Girl&03", + "value": "1237" + }, + { + "label": "GreedyProt", + "value": "1263" + }, + { + "label": "GalaxyWars", + "value": "1317" + }, + { + "label": "GroupChat", + "value": "1323" + }, + { + "label": "gravityfal", + "value": "1477" + }, + { + "label": "GodLikeMC", + "value": "1480" + }, + { + "label": "Grupchat", + "value": "1514" + }, + { + "label": "GodandDevi", + "value": "1544" + }, + { + "label": "Genshin", + "value": "1546" + }, + { + "label": "Gourmet", + "value": "1587" + }, + { + "label": "Goddess", + "value": "1611" + }, + { + "label": "GodlyPower", + "value": "1618" + }, + { + "label": "Geass", + "value": "1626" + }, + { + "label": "Government", + "value": "1653" + }, + { + "label": "gameelemen", + "value": "1665" + }, + { + "label": "Genderless", + "value": "1670" + }, + { + "label": "Godzilla", + "value": "1753" + }, + { + "label": "GetRich", + "value": "1760" + }, + { + "label": "GentleProt", + "value": "1765" + }, + { + "label": "GentleLove", + "value": "1792" + }, + { + "label": "greentea", + "value": "1818" + }, + { + "label": "GradeXNUMX", + "value": "1837" + }, + { + "label": "GuShaoxia", + "value": "1887" + }, + { + "label": "goslowbro", + "value": "1900" + }, + { + "label": "goodpotdre", + "value": "1909" + }, + { + "label": "godofduel", + "value": "1948" + }, + { + "label": "Galacticos", + "value": "1958" + }, + { + "label": "goldfinger", + "value": "1980" + }, + { + "label": "Go", + "value": "1981" + }, + { + "label": "gentleman", + "value": "2027" + }, + { + "label": "GodofForti", + "value": "2037" + }, + { + "label": "GreatSage", + "value": "2075" + }, + { + "label": "gossip", + "value": "2088" + }, + { + "label": "giveyoutim", + "value": "2094" + }, + { + "label": "GoneStrawb", + "value": "2116" + }, + { + "label": "Gotaki", + "value": "2129" + }, + { + "label": "God&039", + "value": "2210" + }, + { + "label": "Galaxyboy", + "value": "2282" + }, + { + "label": "GreatCeles", + "value": "2353" + }, + { + "label": "Godofwings", + "value": "2366" + }, + { + "label": "GLL", + "value": "2372" + }, + { + "label": "goddessbos", + "value": "2411" + }, + { + "label": "Ghostsinre", + "value": "2441" + }, + { + "label": "Goddidnotg", + "value": "2451" + }, + { + "label": "Gooifyouca", + "value": "2533" + }, + { + "label": "GuiltyScis", + "value": "2640" + }, + { + "label": "godsaltedf", + "value": "2650" + }, + { + "label": "Golden", + "value": "2699" + }, + { + "label": "good-natur", + "value": "2712" + }, + { + "label": "goallist", + "value": "2725" + }, + { + "label": "GaoYuanyao", + "value": "2727" + }, + { + "label": "Ghostexter", + "value": "2740" + }, + { + "label": "goldenfore", + "value": "2810" + }, + { + "label": "GeneralXie", + "value": "2837" + }, + { + "label": "giantpanda", + "value": "2902" + }, + { + "label": "GroupPet", + "value": "2954" + }, + { + "label": "GingerLemo", + "value": "2985" + }, + { + "label": "GameLit", + "value": "3041" + }, + { + "label": "gongregret", + "value": "3064" + }, + { + "label": "goldrush", + "value": "3081" + }, + { + "label": "gacha", + "value": "3211" + }, + { + "label": "grudges", + "value": "3272" + }, + { + "label": "Gangster", + "value": "3279" + }, + { + "label": "Grandpa", + "value": "3290" + }, + { + "label": "HandsomeMa", + "value": "177" + }, + { + "label": "Harem", + "value": "157" + }, + { + "label": "Heartwarmi", + "value": "450" + }, + { + "label": "HidingTrue", + "value": "231" + }, + { + "label": "HidingTrue", + "value": "243" + }, + { + "label": "HiddenAbil", + "value": "215" + }, + { + "label": "HarryPotte", + "value": "185" + }, + { + "label": "Heroes", + "value": "551" + }, + { + "label": "Historical", + "value": "828" + }, + { + "label": "Hero", + "value": "26" + }, + { + "label": "Hackers", + "value": "205" + }, + { + "label": "Hunters", + "value": "80" + }, + { + "label": "HumanoidPr", + "value": "629" + }, + { + "label": "HotBlood", + "value": "2977" + }, + { + "label": "HeavenlyTr", + "value": "529" + }, + { + "label": "horror", + "value": "825" + }, + { + "label": "HumanExper", + "value": "407" + }, + { + "label": "highiq", + "value": "1475" + }, + { + "label": "HiddenTrue", + "value": "1154" + }, + { + "label": "HunterxHun", + "value": "1391" + }, + { + "label": "HatedProta", + "value": "477" + }, + { + "label": "HonestProt", + "value": "492" + }, + { + "label": "HappyEndin", + "value": "838" + }, + { + "label": "HighFantas", + "value": "1719" + }, + { + "label": "Healers", + "value": "719" + }, + { + "label": "Hunter", + "value": "3030" + }, + { + "label": "HidingTrue", + "value": "1134" + }, + { + "label": "Hell", + "value": "549" + }, + { + "label": "HelpfulPro", + "value": "376" + }, + { + "label": "Hacker", + "value": "1076" + }, + { + "label": "Heaven", + "value": "548" + }, + { + "label": "HeroandDem", + "value": "988" + }, + { + "label": "HarshTrain", + "value": "554" + }, + { + "label": "Hospital", + "value": "572" + }, + { + "label": "Handjob", + "value": "782" + }, + { + "label": "Hollywood", + "value": "935" + }, + { + "label": "Healing", + "value": "1511" + }, + { + "label": "HiddenIden", + "value": "1614" + }, + { + "label": "Horor", + "value": "3294" + }, + { + "label": "HumanWeapo", + "value": "742" + }, + { + "label": "Hypnotism", + "value": "785" + }, + { + "label": "Homeaffair", + "value": "968" + }, + { + "label": "HomeDrama", + "value": "976" + }, + { + "label": "Heartful", + "value": "985" + }, + { + "label": "HighSchool", + "value": "1053" + }, + { + "label": "HandsomePr", + "value": "1241" + }, + { + "label": "HonkaiImpa", + "value": "1425" + }, + { + "label": "HighSchool", + "value": "1672" + }, + { + "label": "Hndjob", + "value": "784" + }, + { + "label": "Halo", + "value": "821" + }, + { + "label": "HxH", + "value": "1105" + }, + { + "label": "Hard-Worki", + "value": "178" + }, + { + "label": "Harem-seek", + "value": "195" + }, + { + "label": "Human-Nonh", + "value": "327" + }, + { + "label": "Hot-bloode", + "value": "540" + }, + { + "label": "Half-human", + "value": "557" + }, + { + "label": "HidingAbil", + "value": "712" + }, + { + "label": "Hotels", + "value": "872" + }, + { + "label": "HJGrandPri", + "value": "994" + }, + { + "label": "HardBoiled", + "value": "1027" + }, + { + "label": "Homunculus", + "value": "1029" + }, + { + "label": "HidingTrue", + "value": "1077" + }, + { + "label": "history", + "value": "1131" + }, + { + "label": "HiddenYrue", + "value": "1180" + }, + { + "label": "Happy", + "value": "1202" + }, + { + "label": "Hardworkin", + "value": "1218" + }, + { + "label": "hiddenvest", + "value": "1252" + }, + { + "label": "HaremSeeki", + "value": "1257" + }, + { + "label": "HiddenIden", + "value": "1262" + }, + { + "label": "HumanExper", + "value": "1441" + }, + { + "label": "harrypotte", + "value": "1474" + }, + { + "label": "HiddenBoss", + "value": "1520" + }, + { + "label": "Hogwarts", + "value": "1644" + }, + { + "label": "HandsomeMa", + "value": "1696" + }, + { + "label": "Heterochro", + "value": "1701" + }, + { + "label": "Haikyuu", + "value": "1810" + }, + { + "label": "heroine", + "value": "1819" + }, + { + "label": "HongmengSh", + "value": "1823" + }, + { + "label": "HolyKingRa", + "value": "1862" + }, + { + "label": "HongTang", + "value": "1924" + }, + { + "label": "hunterkill", + "value": "1927" + }, + { + "label": "hyperknigh", + "value": "1965" + }, + { + "label": "Heroesofth", + "value": "1999" + }, + { + "label": "hi", + "value": "2034" + }, + { + "label": "HuiMochou", + "value": "2035" + }, + { + "label": "holyangel", + "value": "2051" + }, + { + "label": "HuanHuanHu", + "value": "2107" + }, + { + "label": "hey", + "value": "2134" + }, + { + "label": "Hashihime", + "value": "2137" + }, + { + "label": "Higu", + "value": "2145" + }, + { + "label": "HappyBeanl", + "value": "2213" + }, + { + "label": "酣歌", + "value": "2220" + }, + { + "label": "Honghuangs", + "value": "2253" + }, + { + "label": "humla", + "value": "2258" + }, + { + "label": "Huijingund", + "value": "2277" + }, + { + "label": "howlingpig", + "value": "2351" + }, + { + "label": "Healthewor", + "value": "2358" + }, + { + "label": "Hawkeye", + "value": "2364" + }, + { + "label": "HaotianExt", + "value": "2397" + }, + { + "label": "handtearin", + "value": "2399" + }, + { + "label": "HomeAttrib", + "value": "2407" + }, + { + "label": "HuTiandi", + "value": "2426" + }, + { + "label": "horrorgod", + "value": "2430" + }, + { + "label": "HakoniwaSe", + "value": "2438" + }, + { + "label": "houseprope", + "value": "2452" + }, + { + "label": "HisMajesty", + "value": "2472" + }, + { + "label": "halfstepge", + "value": "2522" + }, + { + "label": "HonghuangN", + "value": "2574" + }, + { + "label": "Haremismta", + "value": "2580" + }, + { + "label": "heavenclea", + "value": "2588" + }, + { + "label": "heavensong", + "value": "2594" + }, + { + "label": "HongfeiQin", + "value": "2659" + }, + { + "label": "handsomeon", + "value": "2669" + }, + { + "label": "heartandey", + "value": "2678" + }, + { + "label": "halfanoran", + "value": "2679" + }, + { + "label": "HonestandR", + "value": "2688" + }, + { + "label": "HeartHunte", + "value": "2741" + }, + { + "label": "Haminstant", + "value": "2753" + }, + { + "label": "hotpot", + "value": "2783" + }, + { + "label": "H11H", + "value": "2798" + }, + { + "label": "howlingwin", + "value": "2840" + }, + { + "label": "Handsomegu", + "value": "2851" + }, + { + "label": "HappyFlow", + "value": "2886" + }, + { + "label": "Hegemony", + "value": "2944" + }, + { + "label": "Hunter×Hu", + "value": "2961" + }, + { + "label": "hitten", + "value": "2973" + }, + { + "label": "HaoyuYingx", + "value": "2986" + }, + { + "label": "HardSci-fi", + "value": "3003" + }, + { + "label": "HeartBreak", + "value": "3083" + }, + { + "label": "Heartthrob", + "value": "3172" + }, + { + "label": "Hiddenmarr", + "value": "3187" + }, + { + "label": "Hikusei", + "value": "3246" + }, + { + "label": "Immortals", + "value": "79" + }, + { + "label": "Isekai", + "value": "846" + }, + { + "label": "Interstell", + "value": "870" + }, + { + "label": "ImperialHa", + "value": "193" + }, + { + "label": "Incest", + "value": "341" + }, + { + "label": "Inheritanc", + "value": "553" + }, + { + "label": "Industrial", + "value": "535" + }, + { + "label": "Interestel", + "value": "897" + }, + { + "label": "Insects", + "value": "257" + }, + { + "label": "Immortal", + "value": "1123" + }, + { + "label": "Inferiorit", + "value": "611" + }, + { + "label": "Invincible", + "value": "3199" + }, + { + "label": "Investigat", + "value": "698" + }, + { + "label": "Infrastruc", + "value": "1433" + }, + { + "label": "Indonesia", + "value": "1718" + }, + { + "label": "IdentityCr", + "value": "724" + }, + { + "label": "IsekaiBatt", + "value": "977" + }, + { + "label": "Inscriptio", + "value": "727" + }, + { + "label": "Inuyasha", + "value": "938" + }, + { + "label": "IndonesiaN", + "value": "1717" + }, + { + "label": "Industry", + "value": "1321" + }, + { + "label": "InfiniteFl", + "value": "1344" + }, + { + "label": "ImperialFa", + "value": "1135" + }, + { + "label": "Investigat", + "value": "1214" + }, + { + "label": "Idol", + "value": "1353" + }, + { + "label": "infinite", + "value": "1418" + }, + { + "label": "InnerVoice", + "value": "1677" + }, + { + "label": "Interdimen", + "value": "357" + }, + { + "label": "Indecisive", + "value": "431" + }, + { + "label": "Introverte", + "value": "452" + }, + { + "label": "InfinitySt", + "value": "957" + }, + { + "label": "Intimate", + "value": "1011" + }, + { + "label": "Interconne", + "value": "1056" + }, + { + "label": "infrastrac", + "value": "1424" + }, + { + "label": "imperialco", + "value": "1510" + }, + { + "label": "IsItWrongt", + "value": "1725" + }, + { + "label": "Illigitima", + "value": "1757" + }, + { + "label": "Ilo", + "value": "1829" + }, + { + "label": "Invincible", + "value": "1849" + }, + { + "label": "idropbaby", + "value": "1878" + }, + { + "label": "Invincible", + "value": "1913" + }, + { + "label": "Invincible", + "value": "1922" + }, + { + "label": "Iamtheseak", + "value": "1943" + }, + { + "label": "Ijustwantt", + "value": "1944" + }, + { + "label": "iamatravel", + "value": "1966" + }, + { + "label": "Invincible", + "value": "1993" + }, + { + "label": "icewalk", + "value": "2009" + }, + { + "label": "Intercept0", + "value": "2029" + }, + { + "label": "Iliveupsta", + "value": "2053" + }, + { + "label": "Iamnotaloc", + "value": "2054" + }, + { + "label": "Infiniteme", + "value": "2073" + }, + { + "label": "icalledthe", + "value": "2076" + }, + { + "label": "Infernalco", + "value": "2112" + }, + { + "label": "isitnecess", + "value": "2114" + }, + { + "label": "Isuckbrown", + "value": "2140" + }, + { + "label": "Itsdaybrea", + "value": "2151" + }, + { + "label": "Iwishyouat", + "value": "2157" + }, + { + "label": "Ifyoucango", + "value": "2158" + }, + { + "label": "It&039s", + "value": "2218" + }, + { + "label": "I&039ma", + "value": "2227" + }, + { + "label": "InfiniteBu", + "value": "2228" + }, + { + "label": "idon&03", + "value": "2232" + }, + { + "label": "Iamolderth", + "value": "2309" + }, + { + "label": "IamHisMaje", + "value": "2337" + }, + { + "label": "Iamarealdi", + "value": "2346" + }, + { + "label": "Iamfifth", + "value": "2356" + }, + { + "label": "Iamapirate", + "value": "2362" + }, + { + "label": "ironpillar", + "value": "2373" + }, + { + "label": "I&039mo", + "value": "2390" + }, + { + "label": "IamGuanxi", + "value": "2466" + }, + { + "label": "Iamhell", + "value": "2482" + }, + { + "label": "iwantmoney", + "value": "2507" + }, + { + "label": "IsumiLily", + "value": "2529" + }, + { + "label": "Iwanttobea", + "value": "2543" + }, + { + "label": "Iwanttobeo", + "value": "2579" + }, + { + "label": "infinitesu", + "value": "2590" + }, + { + "label": "Ibuprofen", + "value": "2639" + }, + { + "label": "Iamtwenty-", + "value": "2663" + }, + { + "label": "ieatgrass", + "value": "2670" + }, + { + "label": "iwanttogot", + "value": "2701" + }, + { + "label": "Iamtheseco", + "value": "2707" + }, + { + "label": "ImmortalBi", + "value": "2717" + }, + { + "label": "IronThanos", + "value": "2730" + }, + { + "label": "Iamoldwolf", + "value": "2757" + }, + { + "label": "ilovewoo", + "value": "2764" + }, + { + "label": "IamAsi", + "value": "2806" + }, + { + "label": "ihavethere", + "value": "2829" + }, + { + "label": "Iamthemurd", + "value": "2835" + }, + { + "label": "ImmortalMa", + "value": "2836" + }, + { + "label": "Ink", + "value": "2861" + }, + { + "label": "Intercept0", + "value": "2865" + }, + { + "label": "iwanttoeat", + "value": "2876" + }, + { + "label": "insitu", + "value": "2880" + }, + { + "label": "IcedDurian", + "value": "2888" + }, + { + "label": "IronMaiden", + "value": "2966" + }, + { + "label": "Iateeightc", + "value": "2969" + }, + { + "label": "ImperialEx", + "value": "3010" + }, + { + "label": "industrial", + "value": "3028" + }, + { + "label": "Inferior", + "value": "3084" + }, + { + "label": "industryel", + "value": "3138" + }, + { + "label": "Imposter", + "value": "3142" + }, + { + "label": "ImmortalEm", + "value": "3253" + }, + { + "label": "Invincible", + "value": "3266" + }, + { + "label": "Japanese", + "value": "1710" + }, + { + "label": "JackofAllT", + "value": "196" + }, + { + "label": "Jealousy", + "value": "448" + }, + { + "label": "Journeytot", + "value": "1379" + }, + { + "label": "Josei", + "value": "856" + }, + { + "label": "JujutsuKai", + "value": "1171" + }, + { + "label": "Jiangshi", + "value": "604" + }, + { + "label": "JoblessCla", + "value": "1052" + }, + { + "label": "JoJo", + "value": "2921" + }, + { + "label": "juvenile", + "value": "3228" + }, + { + "label": "JapIdols", + "value": "873" + }, + { + "label": "JojoBizarr", + "value": "1170" + }, + { + "label": "Jianghu", + "value": "1306" + }, + { + "label": "Japan", + "value": "1354" + }, + { + "label": "JangSeok-g", + "value": "1834" + }, + { + "label": "Juliet", + "value": "1846" + }, + { + "label": "JOJOWE", + "value": "1998" + }, + { + "label": "joydrummer", + "value": "2050" + }, + { + "label": "Jiutianyu", + "value": "2211" + }, + { + "label": "JuniorSist", + "value": "2219" + }, + { + "label": "jadeeveryy", + "value": "2378" + }, + { + "label": "Jianjiamix", + "value": "2422" + }, + { + "label": "jellyjelly", + "value": "2542" + }, + { + "label": "John117", + "value": "2704" + }, + { + "label": "Jazz", + "value": "2746" + }, + { + "label": "JinglongTa", + "value": "2820" + }, + { + "label": "JunCaiXing", + "value": "2854" + }, + { + "label": "justshout", + "value": "2858" + }, + { + "label": "JoJo&03", + "value": "2922" + }, + { + "label": "JackieChan", + "value": "2931" + }, + { + "label": "家族", + "value": "3234" + }, + { + "label": "Korean", + "value": "1038" + }, + { + "label": "KingdomBui", + "value": "71" + }, + { + "label": "KoreanNove", + "value": "1362" + }, + { + "label": "Kingdoms", + "value": "66" + }, + { + "label": "Knights", + "value": "15" + }, + { + "label": "Kingdom-bu", + "value": "1195" + }, + { + "label": "KindLoveIn", + "value": "652" + }, + { + "label": "Kidnapping", + "value": "310" + }, + { + "label": "killdecisi", + "value": "3235" + }, + { + "label": "Killer", + "value": "3143" + }, + { + "label": "Kuudere", + "value": "478" + }, + { + "label": "Knowledgeo", + "value": "924" + }, + { + "label": "KnightsLev", + "value": "563" + }, + { + "label": "KingdomsKn", + "value": "451" + }, + { + "label": "Knight", + "value": "865" + }, + { + "label": "KpopIdols", + "value": "874" + }, + { + "label": "KindProtag", + "value": "1501" + }, + { + "label": "King", + "value": "1935" + }, + { + "label": "Kojin", + "value": "2255" + }, + { + "label": "kendo", + "value": "3219" + }, + { + "label": "KingdomBui", + "value": "153" + }, + { + "label": "KurokonoBa", + "value": "926" + }, + { + "label": "KamenRider", + "value": "956" + }, + { + "label": "K-popIdols", + "value": "1150" + }, + { + "label": "KindomBuil", + "value": "1499" + }, + { + "label": "Koi", + "value": "1615" + }, + { + "label": "Kingdom", + "value": "1667" + }, + { + "label": "Kindergart", + "value": "1796" + }, + { + "label": "KingofDest", + "value": "1863" + }, + { + "label": "KnifePromi", + "value": "2044" + }, + { + "label": "KurongTemp", + "value": "2046" + }, + { + "label": "Kafkajumpi", + "value": "2074" + }, + { + "label": "KwunTong", + "value": "2081" + }, + { + "label": "kingpirate", + "value": "2225" + }, + { + "label": "Killthewor", + "value": "2233" + }, + { + "label": "Kneelingth", + "value": "2344" + }, + { + "label": "KingAsura", + "value": "2431" + }, + { + "label": "KingofMons", + "value": "2475" + }, + { + "label": "keytocome", + "value": "2492" + }, + { + "label": "Knowtheric", + "value": "2519" + }, + { + "label": "Kiritani", + "value": "2576" + }, + { + "label": "KonohaVoll", + "value": "2591" + }, + { + "label": "kingofdeat", + "value": "2654" + }, + { + "label": "KingKonggo", + "value": "2697" + }, + { + "label": "Kneelingan", + "value": "2857" + }, + { + "label": "空间", + "value": "3288" + }, + { + "label": "LightNovel", + "value": "1711" + }, + { + "label": "LevelSyste", + "value": "33" + }, + { + "label": "LuckyProta", + "value": "332" + }, + { + "label": "Livebroadc", + "value": "886" + }, + { + "label": "LateRomanc", + "value": "83" + }, + { + "label": "LoyalSubor", + "value": "389" + }, + { + "label": "Levelup", + "value": "3141" + }, + { + "label": "LazyProtag", + "value": "303" + }, + { + "label": "LackofComm", + "value": "277" + }, + { + "label": "LoveatFirs", + "value": "494" + }, + { + "label": "Lolicon", + "value": "29" + }, + { + "label": "Low-keyPro", + "value": "244" + }, + { + "label": "LitRPG", + "value": "1080" + }, + { + "label": "Loli", + "value": "630" + }, + { + "label": "Leadership", + "value": "345" + }, + { + "label": "LoversReun", + "value": "438" + }, + { + "label": "LonerProta", + "value": "453" + }, + { + "label": "LoveRivals", + "value": "677" + }, + { + "label": "LongSepara", + "value": "493" + }, + { + "label": "LoveTriang", + "value": "646" + }, + { + "label": "Lawyers", + "value": "358" + }, + { + "label": "LoveComedy", + "value": "950" + }, + { + "label": "LowkeyProt", + "value": "1229" + }, + { + "label": "LordoftheM", + "value": "1196" + }, + { + "label": "Life", + "value": "3085" + }, + { + "label": "LimitedLif", + "value": "578" + }, + { + "label": "LivingAlon", + "value": "602" + }, + { + "label": "Legends", + "value": "792" + }, + { + "label": "LiveStream", + "value": "851" + }, + { + "label": "ListCreati", + "value": "1388" + }, + { + "label": "Lovetriang", + "value": "3168" + }, + { + "label": "Lottery", + "value": "393" + }, + { + "label": "Love", + "value": "1535" + }, + { + "label": "LoveContra", + "value": "1612" + }, + { + "label": "LostCivili", + "value": "789" + }, + { + "label": "literature", + "value": "1463" + }, + { + "label": "Luck", + "value": "1616" + }, + { + "label": "LiveStream", + "value": "1640" + }, + { + "label": "LowFantasy", + "value": "1688" + }, + { + "label": "LuckPlunde", + "value": "1763" + }, + { + "label": "LifeScript", + "value": "1764" + }, + { + "label": "LordAbilit", + "value": "1767" + }, + { + "label": "LiWudi", + "value": "1915" + }, + { + "label": "Lillie", + "value": "2709" + }, + { + "label": "LimitlessF", + "value": "124" + }, + { + "label": "literature", + "value": "126" + }, + { + "label": "LoveIntere", + "value": "184" + }, + { + "label": "Library", + "value": "568" + }, + { + "label": "Legend", + "value": "682" + }, + { + "label": "Long-dista", + "value": "783" + }, + { + "label": "LeagueofLe", + "value": "845" + }, + { + "label": "Luxury", + "value": "875" + }, + { + "label": "LiveBroadc", + "value": "912" + }, + { + "label": "Loner", + "value": "1068" + }, + { + "label": "LowKeyMc", + "value": "1139" + }, + { + "label": "Liar", + "value": "1209" + }, + { + "label": "LoyalProta", + "value": "1341" + }, + { + "label": "LoveandMar", + "value": "1434" + }, + { + "label": "lovingfami", + "value": "1446" + }, + { + "label": "lightnovel", + "value": "1454" + }, + { + "label": "lgbt", + "value": "1476" + }, + { + "label": "LoliProtag", + "value": "1554" + }, + { + "label": "LordGodSpa", + "value": "1624" + }, + { + "label": "LoyalSurbo", + "value": "1635" + }, + { + "label": "LoveIntere", + "value": "1662" + }, + { + "label": "LoveIntere", + "value": "1699" + }, + { + "label": "lesstime", + "value": "1883" + }, + { + "label": "LuoTianyi", + "value": "1904" + }, + { + "label": "LikeaDrago", + "value": "1942" + }, + { + "label": "LikeaDrago", + "value": "1945" + }, + { + "label": "Loseafewpo", + "value": "1963" + }, + { + "label": "LeiJiedoes", + "value": "1971" + }, + { + "label": "LordoftheS", + "value": "2019" + }, + { + "label": "Lonelynota", + "value": "2023" + }, + { + "label": "LuoWei", + "value": "2033" + }, + { + "label": "littlehson", + "value": "2040" + }, + { + "label": "LonelyCity", + "value": "2049" + }, + { + "label": "littlezlov", + "value": "2068" + }, + { + "label": "LiverPigeo", + "value": "2072" + }, + { + "label": "littlemoon", + "value": "2080" + }, + { + "label": "LaoLaoXu", + "value": "2110" + }, + { + "label": "Lingran", + "value": "2127" + }, + { + "label": "LacquerNig", + "value": "2139" + }, + { + "label": "lazydevil", + "value": "2159" + }, + { + "label": "LuDehua", + "value": "2168" + }, + { + "label": "littledemo", + "value": "2171" + }, + { + "label": "littlefox", + "value": "2189" + }, + { + "label": "Lightnings", + "value": "2203" + }, + { + "label": "Lovesnacks", + "value": "2222" + }, + { + "label": "littleahxi", + "value": "2236" + }, + { + "label": "laurel", + "value": "2241" + }, + { + "label": "LoneCloudP", + "value": "2275" + }, + { + "label": "lackofboat", + "value": "2302" + }, + { + "label": "LiMumu", + "value": "2322" + }, + { + "label": "LongMengme", + "value": "2324" + }, + { + "label": "lemonandsi", + "value": "2327" + }, + { + "label": "littlebrot", + "value": "2331" + }, + { + "label": "LameHaoisa", + "value": "2345" + }, + { + "label": "littlesuns", + "value": "2359" + }, + { + "label": "LordZhangj", + "value": "2371" + }, + { + "label": "Leapeveryd", + "value": "2381" + }, + { + "label": "littledevi", + "value": "2393" + }, + { + "label": "Longpigeon", + "value": "2427" + }, + { + "label": "littlefing", + "value": "2429" + }, + { + "label": "LiuYujun", + "value": "2458" + }, + { + "label": "langyalist", + "value": "2469" + }, + { + "label": "Leisurelys", + "value": "2471" + }, + { + "label": "Longliveth", + "value": "2480" + }, + { + "label": "longlivemy", + "value": "2491" + }, + { + "label": "LY", + "value": "2504" + }, + { + "label": "lonelyboy", + "value": "2541" + }, + { + "label": "laborhonor", + "value": "2586" + }, + { + "label": "LuoXIV", + "value": "2593" + }, + { + "label": "littlewind", + "value": "2611" + }, + { + "label": "Longlivesa", + "value": "2618" + }, + { + "label": "LinZhengyi", + "value": "2631" + }, + { + "label": "LikeMeiAox", + "value": "2643" + }, + { + "label": "LordTiansh", + "value": "2671" + }, + { + "label": "LingshanIs", + "value": "2715" + }, + { + "label": "Longliveth", + "value": "2718" + }, + { + "label": "LinXiufigh", + "value": "2723" + }, + { + "label": "listentoth", + "value": "2782" + }, + { + "label": "LinBei", + "value": "2785" + }, + { + "label": "lu11034363", + "value": "2799" + }, + { + "label": "Lindentree", + "value": "2808" + }, + { + "label": "LuoYuqianq", + "value": "2814" + }, + { + "label": "LiJunhao", + "value": "2816" + }, + { + "label": "lovetease", + "value": "2847" + }, + { + "label": "littledete", + "value": "2866" + }, + { + "label": "Low-keylux", + "value": "2877" + }, + { + "label": "littlecray", + "value": "2878" + }, + { + "label": "lendmefive", + "value": "2898" + }, + { + "label": "littlewate", + "value": "2910" + }, + { + "label": "longlivedm", + "value": "2916" + }, + { + "label": "零充", + "value": "2965" + }, + { + "label": "LeiXunqing", + "value": "2980" + }, + { + "label": "Lord", + "value": "3005" + }, + { + "label": "LoyalSubor", + "value": "3006" + }, + { + "label": "lazy", + "value": "3020" + }, + { + "label": "LittleWhit", + "value": "3029" + }, + { + "label": "LaidBack", + "value": "3044" + }, + { + "label": "ListAdvent", + "value": "3054" + }, + { + "label": "LongStrip", + "value": "3055" + }, + { + "label": "Lucky", + "value": "3066" + }, + { + "label": "Loveafterm", + "value": "3203" + }, + { + "label": "MaleProtag", + "value": "187" + }, + { + "label": "Magic", + "value": "1" + }, + { + "label": "ModernDay", + "value": "179" + }, + { + "label": "Monsters", + "value": "7" + }, + { + "label": "Misunderst", + "value": "1482" + }, + { + "label": "Misunderst", + "value": "276" + }, + { + "label": "ModernWorl", + "value": "36" + }, + { + "label": "Marvel", + "value": "385" + }, + { + "label": "MultipleRe", + "value": "85" + }, + { + "label": "Marriage", + "value": "254" + }, + { + "label": "Military", + "value": "139" + }, + { + "label": "MagicalSpa", + "value": "219" + }, + { + "label": "Martialart", + "value": "197" + }, + { + "label": "Mystery", + "value": "904" + }, + { + "label": "Modern", + "value": "519" + }, + { + "label": "ModernKnow", + "value": "320" + }, + { + "label": "MultiplePO", + "value": "504" + }, + { + "label": "Mpreg", + "value": "255" + }, + { + "label": "MedicalKno", + "value": "275" + }, + { + "label": "MMORPG", + "value": "44" + }, + { + "label": "ModernFant", + "value": "1172" + }, + { + "label": "MonsterTam", + "value": "258" + }, + { + "label": "Mysterious", + "value": "510" + }, + { + "label": "MagicBeast", + "value": "317" + }, + { + "label": "Medieval", + "value": "537" + }, + { + "label": "MaleYander", + "value": "428" + }, + { + "label": "MoneyGrubb", + "value": "330" + }, + { + "label": "MartialSpi", + "value": "246" + }, + { + "label": "Mythology", + "value": "499" + }, + { + "label": "MultipleId", + "value": "328" + }, + { + "label": "MysterySol", + "value": "372" + }, + { + "label": "Munchkin", + "value": "3212" + }, + { + "label": "Movies", + "value": "119" + }, + { + "label": "MythicalBe", + "value": "333" + }, + { + "label": "MatureProt", + "value": "564" + }, + { + "label": "MiddleAges", + "value": "3205" + }, + { + "label": "music", + "value": "127" + }, + { + "label": "Mecha", + "value": "162" + }, + { + "label": "MagicForma", + "value": "394" + }, + { + "label": "MarvelUniv", + "value": "386" + }, + { + "label": "ManlyGayCo", + "value": "733" + }, + { + "label": "MagicalTec", + "value": "318" + }, + { + "label": "Management", + "value": "400" + }, + { + "label": "Mercenarie", + "value": "586" + }, + { + "label": "MultiplePr", + "value": "674" + }, + { + "label": "MutatedCre", + "value": "460" + }, + { + "label": "MaletoFema", + "value": "631" + }, + { + "label": "Medicine", + "value": "81" + }, + { + "label": "Maids", + "value": "502" + }, + { + "label": "Murders", + "value": "679" + }, + { + "label": "Mutations", + "value": "634" + }, + { + "label": "Mutation", + "value": "132" + }, + { + "label": "MindContro", + "value": "206" + }, + { + "label": "MaleLead", + "value": "655" + }, + { + "label": "Merchants", + "value": "695" + }, + { + "label": "modernlove", + "value": "1821" + }, + { + "label": "Mature", + "value": "3017" + }, + { + "label": "MonsterGir", + "value": "223" + }, + { + "label": "MobProtago", + "value": "484" + }, + { + "label": "MyHeroAcad", + "value": "1393" + }, + { + "label": "Myth", + "value": "3178" + }, + { + "label": "ModernDays", + "value": "667" + }, + { + "label": "MangaUP!Aw", + "value": "978" + }, + { + "label": "MultipleWo", + "value": "151" + }, + { + "label": "Models", + "value": "439" + }, + { + "label": "Murder", + "value": "565" + }, + { + "label": "Matriarchy", + "value": "582" + }, + { + "label": "MultipleTi", + "value": "672" + }, + { + "label": "MuteCharac", + "value": "721" + }, + { + "label": "MassiveHar", + "value": "862" + }, + { + "label": "Mafia", + "value": "961" + }, + { + "label": "MartialArt", + "value": "1508" + }, + { + "label": "MaleProtag", + "value": "2940" + }, + { + "label": "Mysterious", + "value": "377" + }, + { + "label": "Mythical", + "value": "498" + }, + { + "label": "Masturbati", + "value": "645" + }, + { + "label": "Malaysian", + "value": "1716" + }, + { + "label": "MrMo", + "value": "1840" + }, + { + "label": "Mercenary", + "value": "140" + }, + { + "label": "Mystical", + "value": "753" + }, + { + "label": "ModernRoma", + "value": "1087" + }, + { + "label": "MaleMc", + "value": "1140" + }, + { + "label": "Monster", + "value": "1181" + }, + { + "label": "mermaid", + "value": "2915" + }, + { + "label": "MingDynast", + "value": "3038" + }, + { + "label": "MultipleCP", + "value": "773" + }, + { + "label": "Mansour", + "value": "1010" + }, + { + "label": "MaleProtag", + "value": "1081" + }, + { + "label": "Male-Lead", + "value": "1400" + }, + { + "label": "MartialSpi", + "value": "1414" + }, + { + "label": "Male-Prota", + "value": "1428" + }, + { + "label": "Multiverse", + "value": "1558" + }, + { + "label": "Mob", + "value": "1620" + }, + { + "label": "Mythos", + "value": "1690" + }, + { + "label": "multiplere", + "value": "1697" + }, + { + "label": "MalaysianN", + "value": "1715" + }, + { + "label": "Meowingbig", + "value": "2126" + }, + { + "label": "ModernLife", + "value": "2948" + }, + { + "label": "mysteries", + "value": "3033" + }, + { + "label": "Medical", + "value": "3074" + }, + { + "label": "Multiplele", + "value": "3186" + }, + { + "label": "MultipleRe", + "value": "180" + }, + { + "label": "MultiplePe", + "value": "256" + }, + { + "label": "Masochisti", + "value": "270" + }, + { + "label": "Master-Dis", + "value": "292" + }, + { + "label": "Mysterious", + "value": "301" + }, + { + "label": "Manipulati", + "value": "346" + }, + { + "label": "MultipleTr", + "value": "455" + }, + { + "label": "Marriageof", + "value": "473" + }, + { + "label": "Master-Ser", + "value": "523" + }, + { + "label": "MindBreak", + "value": "802" + }, + { + "label": "Mangaka", + "value": "817" + }, + { + "label": "ModernKnow", + "value": "829" + }, + { + "label": "MyTeenRoma", + "value": "943" + }, + { + "label": "Martialart", + "value": "954" + }, + { + "label": "MultipleLo", + "value": "992" + }, + { + "label": "Misunderst", + "value": "1118" + }, + { + "label": "Massive", + "value": "1120" + }, + { + "label": "Mysterious", + "value": "1144" + }, + { + "label": "MedicalKno", + "value": "1155" + }, + { + "label": "MagicWorld", + "value": "1158" + }, + { + "label": "MultipleTr", + "value": "1197" + }, + { + "label": "MonsterSoc", + "value": "1206" + }, + { + "label": "Mutualcrus", + "value": "1207" + }, + { + "label": "MultipleMo", + "value": "1212" + }, + { + "label": "MultipleHi", + "value": "1234" + }, + { + "label": "Monogamy", + "value": "1242" + }, + { + "label": "MoneyGrumb", + "value": "1243" + }, + { + "label": "MaleMain-l", + "value": "1253" + }, + { + "label": "Magician", + "value": "1258" + }, + { + "label": "Master-App", + "value": "1274" + }, + { + "label": "Married", + "value": "1281" + }, + { + "label": "Mage", + "value": "1297" + }, + { + "label": "MaleProtag", + "value": "1309" + }, + { + "label": "MultiplePo", + "value": "1329" + }, + { + "label": "Maleprotag", + "value": "1339" + }, + { + "label": "multipleid", + "value": "1347" + }, + { + "label": "Mismatched", + "value": "1355" + }, + { + "label": "MutantPowe", + "value": "1396" + }, + { + "label": "ModerDays", + "value": "1420" + }, + { + "label": "MultipleBo", + "value": "1442" + }, + { + "label": "movies", + "value": "1460" + }, + { + "label": "Middleage", + "value": "1484" + }, + { + "label": "MagicalAbi", + "value": "1490" + }, + { + "label": "Millionair", + "value": "1496" + }, + { + "label": "MultipleVe", + "value": "1507" + }, + { + "label": "Manipulati", + "value": "1517" + }, + { + "label": "MindReader", + "value": "1521" + }, + { + "label": "MorallyAmb", + "value": "1530" + }, + { + "label": "MCStrongFr", + "value": "1545" + }, + { + "label": "MemoryLoss", + "value": "1555" + }, + { + "label": "MarriageCo", + "value": "1579" + }, + { + "label": "Maid", + "value": "1596" + }, + { + "label": "Misunderst", + "value": "1601" + }, + { + "label": "Mutan", + "value": "1610" + }, + { + "label": "Msturbatio", + "value": "1617" + }, + { + "label": "MagicalBat", + "value": "1623" + }, + { + "label": "Ministryof", + "value": "1645" + }, + { + "label": "MrSly", + "value": "1646" + }, + { + "label": "MsPerfect", + "value": "1647" + }, + { + "label": "MultipleWo", + "value": "1654" + }, + { + "label": "Mukbang", + "value": "1671" + }, + { + "label": "Massacre", + "value": "1673" + }, + { + "label": "MultipleLe", + "value": "1689" + }, + { + "label": "MonsterTar", + "value": "1732" + }, + { + "label": "Mimicry", + "value": "1754" + }, + { + "label": "Multipleid", + "value": "1772" + }, + { + "label": "Matchmadei", + "value": "1779" + }, + { + "label": "Morallessp", + "value": "1781" + }, + { + "label": "MemoryReve", + "value": "1783" + }, + { + "label": "magicalgir", + "value": "1787" + }, + { + "label": "MarriedCou", + "value": "1793" + }, + { + "label": "marvelworl", + "value": "1820" + }, + { + "label": "math", + "value": "1822" + }, + { + "label": "Moonwing", + "value": "1828" + }, + { + "label": "MentalIlln", + "value": "1854" + }, + { + "label": "Mountainsa", + "value": "1892" + }, + { + "label": "MaskedAce", + "value": "1903" + }, + { + "label": "musicwilll", + "value": "1956" + }, + { + "label": "Moonsea", + "value": "1964" + }, + { + "label": "moonbug", + "value": "1973" + }, + { + "label": "Mingjiao", + "value": "1984" + }, + { + "label": "MarquisofB", + "value": "1989" + }, + { + "label": "Mr.EasyPro", + "value": "2022" + }, + { + "label": "MingjiaoTi", + "value": "2064" + }, + { + "label": "MoXueqing", + "value": "2069" + }, + { + "label": "慕阳", + "value": "2096" + }, + { + "label": "MynameisDa", + "value": "2117" + }, + { + "label": "meowmeow", + "value": "2120" + }, + { + "label": "movingbean", + "value": "2166" + }, + { + "label": "MarvelKing", + "value": "2173" + }, + { + "label": "Mountainsa", + "value": "2185" + }, + { + "label": "meetthebea", + "value": "2195" + }, + { + "label": "Mistresspl", + "value": "2197" + }, + { + "label": "man", + "value": "2231" + }, + { + "label": "MistyFlyin", + "value": "2234" + }, + { + "label": "mustdo", + "value": "2238" + }, + { + "label": "mudbodhisa", + "value": "2257" + }, + { + "label": "moreandmor", + "value": "2264" + }, + { + "label": "mylittlesi", + "value": "2267" + }, + { + "label": "makeamirac", + "value": "2290" + }, + { + "label": "Masquerade", + "value": "2291" + }, + { + "label": "Miluo", + "value": "2303" + }, + { + "label": "mynameista", + "value": "2307" + }, + { + "label": "milkgrandm", + "value": "2319" + }, + { + "label": "Masterball", + "value": "2320" + }, + { + "label": "MojiaHills", + "value": "2398" + }, + { + "label": "millionord", + "value": "2412" + }, + { + "label": "MagicTides", + "value": "2434" + }, + { + "label": "MojiaAeros", + "value": "2435" + }, + { + "label": "mythunpara", + "value": "2445" + }, + { + "label": "mythicalma", + "value": "2477" + }, + { + "label": "MangoKK", + "value": "2479" + }, + { + "label": "mywife", + "value": "2485" + }, + { + "label": "Moonlikeah", + "value": "2508" + }, + { + "label": "maninnarut", + "value": "2525" + }, + { + "label": "mapleleafb", + "value": "2537" + }, + { + "label": "MarvelPudd", + "value": "2600" + }, + { + "label": "Mr.Huo", + "value": "2605" + }, + { + "label": "Moyangison", + "value": "2632" + }, + { + "label": "mythicalfi", + "value": "2634" + }, + { + "label": "MagicOne", + "value": "2660" + }, + { + "label": "mixedintwo", + "value": "2664" + }, + { + "label": "Mofamily", + "value": "2682" + }, + { + "label": "MyLubanThi", + "value": "2685" + }, + { + "label": "Makeafortu", + "value": "2695" + }, + { + "label": "MoonlightS", + "value": "2721" + }, + { + "label": "MaskedArmo", + "value": "2733" + }, + { + "label": "medicineme", + "value": "2752" + }, + { + "label": "mambafight", + "value": "2780" + }, + { + "label": "mywifeisya", + "value": "2818" + }, + { + "label": "Moedye", + "value": "2853" + }, + { + "label": "MasterofSi", + "value": "2869" + }, + { + "label": "萌图", + "value": "2883" + }, + { + "label": "Milkgather", + "value": "2893" + }, + { + "label": "mindreadin", + "value": "2911" + }, + { + "label": "Malemainch", + "value": "2917" + }, + { + "label": "Merchant", + "value": "2925" + }, + { + "label": "MoeShinkaw", + "value": "2970" + }, + { + "label": "Motherland", + "value": "2974" + }, + { + "label": "Manhua", + "value": "3056" + }, + { + "label": "machine", + "value": "3112" + }, + { + "label": "medicalfem", + "value": "3135" + }, + { + "label": "Marysue", + "value": "3175" + }, + { + "label": "Mag", + "value": "3198" + }, + { + "label": "Morethanju", + "value": "3247" + }, + { + "label": "Mech", + "value": "3271" + }, + { + "label": "myflightat", + "value": "3281" + }, + { + "label": "Martialart", + "value": "3302" + }, + { + "label": "Naruto", + "value": "445" + }, + { + "label": "Nobles", + "value": "16" + }, + { + "label": "Nationalis", + "value": "198" + }, + { + "label": "NaiveProta", + "value": "266" + }, + { + "label": "Noromance", + "value": "1310" + }, + { + "label": "NoCp", + "value": "1641" + }, + { + "label": "Non-humanP", + "value": "847" + }, + { + "label": "NA", + "value": "107" + }, + { + "label": "Necromance", + "value": "72" + }, + { + "label": "Netori", + "value": "115" + }, + { + "label": "Non-System", + "value": "1363" + }, + { + "label": "NonHuman", + "value": "1156" + }, + { + "label": "Ninjas", + "value": "145" + }, + { + "label": "Near-Death", + "value": "290" + }, + { + "label": "No-Harem", + "value": "3181" + }, + { + "label": "Netorare", + "value": "293" + }, + { + "label": "NBA", + "value": "591" + }, + { + "label": "Nudity", + "value": "946" + }, + { + "label": "NoHarem", + "value": "1342" + }, + { + "label": "Nurses", + "value": "805" + }, + { + "label": "NotHarem", + "value": "1030" + }, + { + "label": "NPC", + "value": "3001" + }, + { + "label": "Nightmares", + "value": "643" + }, + { + "label": "Necromancy", + "value": "3127" + }, + { + "label": "Nightmare", + "value": "149" + }, + { + "label": "Navy", + "value": "1145" + }, + { + "label": "NoSystem", + "value": "1573" + }, + { + "label": "Nine-Taile", + "value": "2217" + }, + { + "label": "NoCheats", + "value": "3115" + }, + { + "label": "Neet", + "value": "797" + }, + { + "label": "NoFL", + "value": "936" + }, + { + "label": "NonHumanPr", + "value": "1127" + }, + { + "label": "Noble", + "value": "1636" + }, + { + "label": "nightsilen", + "value": "1934" + }, + { + "label": "Non-humano", + "value": "247" + }, + { + "label": "Narcissist", + "value": "512" + }, + { + "label": "NaturalDis", + "value": "1033" + }, + { + "label": "NationBuil", + "value": "1159" + }, + { + "label": "NoPairing", + "value": "1330" + }, + { + "label": "nonhuman", + "value": "1450" + }, + { + "label": "Napoleon", + "value": "1534" + }, + { + "label": "Non-Humanl", + "value": "1720" + }, + { + "label": "ninja", + "value": "1798" + }, + { + "label": "饕牛", + "value": "1872" + }, + { + "label": "nightfire", + "value": "1902" + }, + { + "label": "Ninjapirat", + "value": "1906" + }, + { + "label": "NinefoldSe", + "value": "1994" + }, + { + "label": "Nosnacks", + "value": "2113" + }, + { + "label": "NiuBao", + "value": "2118" + }, + { + "label": "NineWarsof", + "value": "2122" + }, + { + "label": "Nanshen", + "value": "2347" + }, + { + "label": "NiangkouSa", + "value": "2382" + }, + { + "label": "Nine-color", + "value": "2476" + }, + { + "label": "NarutoQuiz", + "value": "2515" + }, + { + "label": "NarutoClou", + "value": "2516" + }, + { + "label": "narutoceda", + "value": "2520" + }, + { + "label": "notscary", + "value": "2521" + }, + { + "label": "notlevelth", + "value": "2623" + }, + { + "label": "neverfail", + "value": "2627" + }, + { + "label": "noncat", + "value": "2637" + }, + { + "label": "Nidouzi", + "value": "2644" + }, + { + "label": "NarutoxRea", + "value": "2647" + }, + { + "label": "Nowadays", + "value": "2665" + }, + { + "label": "Noless", + "value": "2759" + }, + { + "label": "nightdance", + "value": "2784" + }, + { + "label": "NoGoldFing", + "value": "3104" + }, + { + "label": "NoblesPoli", + "value": "3121" + }, + { + "label": "NTL", + "value": "3124" + }, + { + "label": "Non-HumanM", + "value": "3125" + }, + { + "label": "Novel", + "value": "3305" + }, + { + "label": "OnePiece", + "value": "105" + }, + { + "label": "Obsession", + "value": "3045" + }, + { + "label": "Overpowere", + "value": "3140" + }, + { + "label": "Omegaverse", + "value": "311" + }, + { + "label": "OPMC", + "value": "859" + }, + { + "label": "OlderLoveI", + "value": "207" + }, + { + "label": "OuterSpace", + "value": "371" + }, + { + "label": "OtomeGame", + "value": "944" + }, + { + "label": "Orcs", + "value": "12" + }, + { + "label": "Orphans", + "value": "224" + }, + { + "label": "ObsessiveL", + "value": "520" + }, + { + "label": "OriginalWa", + "value": "997" + }, + { + "label": "Otaku", + "value": "37" + }, + { + "label": "OfficeRoma", + "value": "686" + }, + { + "label": "OrganizedC", + "value": "750" + }, + { + "label": "OnlineRoma", + "value": "637" + }, + { + "label": "OVLGrandPr", + "value": "1000" + }, + { + "label": "OPProtagon", + "value": "1230" + }, + { + "label": "otherworld", + "value": "984" + }, + { + "label": "OnlineGame", + "value": "776" + }, + { + "label": "OldMainCha", + "value": "983" + }, + { + "label": "Organizati", + "value": "1099" + }, + { + "label": "Orphan", + "value": "1293" + }, + { + "label": "onlyloveyo", + "value": "1990" + }, + { + "label": "Overhead", + "value": "3225" + }, + { + "label": "overpower", + "value": "3296" + }, + { + "label": "Overlord", + "value": "1231" + }, + { + "label": "OpFemalePr", + "value": "1235" + }, + { + "label": "Orc", + "value": "1350" + }, + { + "label": "OnePunchMa", + "value": "1380" + }, + { + "label": "overpowere", + "value": "1443" + }, + { + "label": "Operation", + "value": "2936" + }, + { + "label": "Overpowere", + "value": "167" + }, + { + "label": "Overprotec", + "value": "436" + }, + { + "label": "Orientalfa", + "value": "769" + }, + { + "label": "OrphanMC", + "value": "902" + }, + { + "label": "OutdoorInt", + "value": "1031" + }, + { + "label": "OnlineNove", + "value": "1035" + }, + { + "label": "Oneshot", + "value": "1037" + }, + { + "label": "OriginalWa", + "value": "1059" + }, + { + "label": "Orcsworld", + "value": "1089" + }, + { + "label": "Onepunch", + "value": "1106" + }, + { + "label": "Online", + "value": "1162" + }, + { + "label": "Overpowere", + "value": "1260" + }, + { + "label": "onepiece", + "value": "1458" + }, + { + "label": "OPheroine", + "value": "1506" + }, + { + "label": "Overpowere", + "value": "1518" + }, + { + "label": "On-HookSys", + "value": "1651" + }, + { + "label": "OriginalON", + "value": "1686" + }, + { + "label": "openasmall", + "value": "1847" + }, + { + "label": "oldage", + "value": "1949" + }, + { + "label": "OldQinpeop", + "value": "2038" + }, + { + "label": "OneSwordFl", + "value": "2078" + }, + { + "label": "oldfisheat", + "value": "2087" + }, + { + "label": "Onethousan", + "value": "2124" + }, + { + "label": "Otezetta", + "value": "2156" + }, + { + "label": "olddemon", + "value": "2167" + }, + { + "label": "Obanbrothe", + "value": "2191" + }, + { + "label": "orangeappl", + "value": "2281" + }, + { + "label": "onparadise", + "value": "2326" + }, + { + "label": "OriginalUn", + "value": "2367" + }, + { + "label": "Openyourey", + "value": "2370" + }, + { + "label": "oooobe", + "value": "2425" + }, + { + "label": "ohmygod", + "value": "2488" + }, + { + "label": "Oldghostsm", + "value": "2503" + }, + { + "label": "OTTGroupCh", + "value": "2511" + }, + { + "label": "oldtombrob", + "value": "2526" + }, + { + "label": "Onepunchmo", + "value": "2555" + }, + { + "label": "OneLeafRed", + "value": "2559" + }, + { + "label": "oldfaceunc", + "value": "2762" + }, + { + "label": "OriginalYe", + "value": "2766" + }, + { + "label": "Onepunchto", + "value": "2768" + }, + { + "label": "Oneyearold", + "value": "2774" + }, + { + "label": "Oneflower", + "value": "2786" + }, + { + "label": "onetree", + "value": "2787" + }, + { + "label": "onemelonri", + "value": "2788" + }, + { + "label": "onlyyouth", + "value": "2795" + }, + { + "label": "Origuchi", + "value": "2804" + }, + { + "label": "onemeterst", + "value": "2856" + }, + { + "label": "One-Piece", + "value": "2913" + }, + { + "label": "offical", + "value": "2938" + }, + { + "label": "OverheadHi", + "value": "2945" + }, + { + "label": "Official", + "value": "3067" + }, + { + "label": "Olderlovei", + "value": "3088" + }, + { + "label": "Over-Power", + "value": "3090" + }, + { + "label": "omega", + "value": "3099" + }, + { + "label": "Onenightst", + "value": "3176" + }, + { + "label": "Ordinary", + "value": "3217" + }, + { + "label": "Orientalde", + "value": "3263" + }, + { + "label": "Onlinegame", + "value": "3269" + }, + { + "label": "Officialwo", + "value": "3304" + }, + { + "label": "Onlinegame", + "value": "3307" + }, + { + "label": "PoortoRich", + "value": "199" + }, + { + "label": "Possession", + "value": "678" + }, + { + "label": "Politics", + "value": "56" + }, + { + "label": "Polygamy", + "value": "34" + }, + { + "label": "PureLove", + "value": "3052" + }, + { + "label": "Post-apoca", + "value": "59" + }, + { + "label": "Pets", + "value": "148" + }, + { + "label": "Pregnancy", + "value": "297" + }, + { + "label": "Possessive", + "value": "463" + }, + { + "label": "PowerCoupl", + "value": "360" + }, + { + "label": "Pokemon", + "value": "562" + }, + { + "label": "ParallelWo", + "value": "109" + }, + { + "label": "Police", + "value": "566" + }, + { + "label": "PastPlaysa", + "value": "456" + }, + { + "label": "PervertedP", + "value": "515" + }, + { + "label": "PreviousLi", + "value": "457" + }, + { + "label": "PoorProtag", + "value": "220" + }, + { + "label": "PastTrauma", + "value": "648" + }, + { + "label": "Pirates", + "value": "106" + }, + { + "label": "PillConcot", + "value": "91" + }, + { + "label": "PrinceofTe", + "value": "1360" + }, + { + "label": "PopularLov", + "value": "528" + }, + { + "label": "Poisons", + "value": "514" + }, + { + "label": "PillConcoc", + "value": "425" + }, + { + "label": "Powerfulco", + "value": "1405" + }, + { + "label": "PowerStrug", + "value": "531" + }, + { + "label": "Polyandry", + "value": "530" + }, + { + "label": "PragmaticP", + "value": "546" + }, + { + "label": "PlayfulPro", + "value": "560" + }, + { + "label": "ProactiveP", + "value": "718" + }, + { + "label": "Psychologi", + "value": "1122" + }, + { + "label": "Psychopath", + "value": "278" + }, + { + "label": "Possessive", + "value": "3155" + }, + { + "label": "Parody", + "value": "555" + }, + { + "label": "Prison", + "value": "758" + }, + { + "label": "PillBasedC", + "value": "424" + }, + { + "label": "Playboys", + "value": "570" + }, + { + "label": "Priests", + "value": "739" + }, + { + "label": "ParentComp", + "value": "421" + }, + { + "label": "PreviousLi", + "value": "526" + }, + { + "label": "Personalit", + "value": "545" + }, + { + "label": "Pharmacist", + "value": "593" + }, + { + "label": "PsychicPow", + "value": "661" + }, + { + "label": "PortalFant", + "value": "2997" + }, + { + "label": "Prophecies", + "value": "351" + }, + { + "label": "Programmer", + "value": "561" + }, + { + "label": "PoliteProt", + "value": "567" + }, + { + "label": "PretendLov", + "value": "680" + }, + { + "label": "Prostitute", + "value": "740" + }, + { + "label": "ParallelWo", + "value": "826" + }, + { + "label": "Pet", + "value": "1249" + }, + { + "label": "Progressio", + "value": "1691" + }, + { + "label": "Poetry", + "value": "113" + }, + { + "label": "Phoenixes", + "value": "702" + }, + { + "label": "PortableSp", + "value": "918" + }, + { + "label": "Princess", + "value": "1564" + }, + { + "label": "Player", + "value": "1598" + }, + { + "label": "Protagonis", + "value": "1679" + }, + { + "label": "President", + "value": "1758" + }, + { + "label": "Precogniti", + "value": "534" + }, + { + "label": "Part-TimeJ", + "value": "768" + }, + { + "label": "Production", + "value": "1018" + }, + { + "label": "Priestesse", + "value": "1022" + }, + { + "label": "Primitivew", + "value": "1090" + }, + { + "label": "Paranoid", + "value": "1112" + }, + { + "label": "Pirate", + "value": "1213" + }, + { + "label": "Possessive", + "value": "1255" + }, + { + "label": "PerfectWor", + "value": "1385" + }, + { + "label": "PrinceQing", + "value": "2696" + }, + { + "label": "Positive", + "value": "3196" + }, + { + "label": "poems", + "value": "128" + }, + { + "label": "Protagonis", + "value": "248" + }, + { + "label": "Protagonis", + "value": "482" + }, + { + "label": "Persistent", + "value": "623" + }, + { + "label": "Photograph", + "value": "694" + }, + { + "label": "Protagonis", + "value": "715" + }, + { + "label": "PastPlaysa", + "value": "780" + }, + { + "label": "Parasites", + "value": "790" + }, + { + "label": "Philosophi", + "value": "801" + }, + { + "label": "Paizuri", + "value": "812" + }, + { + "label": "PamperingR", + "value": "814" + }, + { + "label": "ParalelWor", + "value": "830" + }, + { + "label": "Professor", + "value": "895" + }, + { + "label": "PoliticalI", + "value": "919" + }, + { + "label": "Porn", + "value": "949" + }, + { + "label": "PoliticalB", + "value": "996" + }, + { + "label": "Pleasure", + "value": "1073" + }, + { + "label": "PowersTran", + "value": "1109" + }, + { + "label": "Psychic", + "value": "1110" + }, + { + "label": "Patriarch", + "value": "1116" + }, + { + "label": "PoisonMout", + "value": "1119" + }, + { + "label": "Protagonis", + "value": "1174" + }, + { + "label": "Plants", + "value": "1185" + }, + { + "label": "Primitives", + "value": "1186" + }, + { + "label": "PacifistPr", + "value": "1233" + }, + { + "label": "Playboy", + "value": "1259" + }, + { + "label": "Painting", + "value": "1275" + }, + { + "label": "Players", + "value": "1291" + }, + { + "label": "Painter", + "value": "1313" + }, + { + "label": "Playboymal", + "value": "1327" + }, + { + "label": "PoliticalS", + "value": "1331" + }, + { + "label": "PovertyAll", + "value": "1332" + }, + { + "label": "PseudoHolo", + "value": "1333" + }, + { + "label": "PseudoReli", + "value": "1334" + }, + { + "label": "Protagonis", + "value": "1357" + }, + { + "label": "Protagonis", + "value": "1394" + }, + { + "label": "Prehistori", + "value": "1402" + }, + { + "label": "Prehistori", + "value": "1412" + }, + { + "label": "Partnerofa", + "value": "1416" + }, + { + "label": "PositiveLe", + "value": "1492" + }, + { + "label": "PlayingGho", + "value": "1519" + }, + { + "label": "Protagonis", + "value": "1562" + }, + { + "label": "Puzzles", + "value": "1566" + }, + { + "label": "PoorRoRich", + "value": "1571" + }, + { + "label": "Psychology", + "value": "1580" + }, + { + "label": "Parasite", + "value": "1590" + }, + { + "label": "Pilots", + "value": "1628" + }, + { + "label": "PlayerKill", + "value": "1733" + }, + { + "label": "PresentDay", + "value": "1775" + }, + { + "label": "Poorcrazy", + "value": "1826" + }, + { + "label": "PeerlessSw", + "value": "1832" + }, + { + "label": "Peerlessso", + "value": "1835" + }, + { + "label": "perfectmag", + "value": "1843" + }, + { + "label": "PirateDaQi", + "value": "1893" + }, + { + "label": "Piratehaha", + "value": "1954" + }, + { + "label": "Punch", + "value": "2004" + }, + { + "label": "part-timeo", + "value": "2063" + }, + { + "label": "pleasantin", + "value": "2093" + }, + { + "label": "PlayBlueMo", + "value": "2104" + }, + { + "label": "pendreamst", + "value": "2132" + }, + { + "label": "Positiveel", + "value": "2138" + }, + { + "label": "plumthirte", + "value": "2188" + }, + { + "label": "PirateGrea", + "value": "2194" + }, + { + "label": "PokémonVo", + "value": "2196" + }, + { + "label": "panic", + "value": "2206" + }, + { + "label": "Pipifish", + "value": "2273" + }, + { + "label": "paleandwhi", + "value": "2301" + }, + { + "label": "purekitten", + "value": "2506" + }, + { + "label": "Pirateacto", + "value": "2528" + }, + { + "label": "PokémonTi", + "value": "2550" + }, + { + "label": "PopeBibiDo", + "value": "2551" + }, + { + "label": "PirateCour", + "value": "2553" + }, + { + "label": "PirateWars", + "value": "2554" + }, + { + "label": "petsurviva", + "value": "2570" + }, + { + "label": "pureimpuls", + "value": "2597" + }, + { + "label": "PiratexFai", + "value": "2613" + }, + { + "label": "pigeonnext", + "value": "2658" + }, + { + "label": "Peoplenear", + "value": "2691" + }, + { + "label": "Papaisvery", + "value": "2703" + }, + { + "label": "Piscesinth", + "value": "2747" + }, + { + "label": "potatogirl", + "value": "2791" + }, + { + "label": "PiratesofH", + "value": "2811" + }, + { + "label": "PokémonGo", + "value": "2827" + }, + { + "label": "pendragon", + "value": "2889" + }, + { + "label": "PrinceofHe", + "value": "2908" + }, + { + "label": "Protagonis", + "value": "2920" + }, + { + "label": "psionic", + "value": "2950" + }, + { + "label": "Pleaseforg", + "value": "2967" + }, + { + "label": "Peasant", + "value": "2979" + }, + { + "label": "PhantomThi", + "value": "2982" + }, + { + "label": "Photograph", + "value": "3021" + }, + { + "label": "Programmin", + "value": "3062" + }, + { + "label": "PlaneWars", + "value": "3072" + }, + { + "label": "PrimitiveT", + "value": "3094" + }, + { + "label": "Poor", + "value": "3117" + }, + { + "label": "Prince", + "value": "3123" + }, + { + "label": "palace", + "value": "3133" + }, + { + "label": "Popular", + "value": "3164" + }, + { + "label": "PrettyGirl", + "value": "3165" + }, + { + "label": "Pretendtob", + "value": "3223" + }, + { + "label": "Pre-Stewar", + "value": "3280" + }, + { + "label": "PastandPre", + "value": "3287" + }, + { + "label": "QuickTrans", + "value": "414" + }, + { + "label": "Qidian", + "value": "933" + }, + { + "label": "QuirkyChar", + "value": "701" + }, + { + "label": "Quickwear", + "value": "1583" + }, + { + "label": "QiLuck", + "value": "1680" + }, + { + "label": "qimao", + "value": "3208" + }, + { + "label": "QuietChara", + "value": "706" + }, + { + "label": "QuickPass", + "value": "1658" + }, + { + "label": "QuickTrans", + "value": "1698" + }, + { + "label": "Question&a", + "value": "1771" + }, + { + "label": "QT", + "value": "1800" + }, + { + "label": "quietflowe", + "value": "1890" + }, + { + "label": "QinBichu", + "value": "1897" + }, + { + "label": "Qingfeng1D", + "value": "1898" + }, + { + "label": "Quasi-GodS", + "value": "1959" + }, + { + "label": "Qingliansw", + "value": "2012" + }, + { + "label": "QingheTaoi", + "value": "2066" + }, + { + "label": "QiXuan", + "value": "2153" + }, + { + "label": "qingyu", + "value": "2400" + }, + { + "label": "QueenofBla", + "value": "2484" + }, + { + "label": "Quququ", + "value": "2677" + }, + { + "label": "QianshanTw", + "value": "2796" + }, + { + "label": "清裁", + "value": "2900" + }, + { + "label": "QinTianhu", + "value": "3256" + }, + { + "label": "Reincarnat", + "value": "20" + }, + { + "label": "Romance", + "value": "774" + }, + { + "label": "Rebirth", + "value": "192" + }, + { + "label": "R18", + "value": "1547" + }, + { + "label": "Revenge", + "value": "42" + }, + { + "label": "Royalty", + "value": "2" + }, + { + "label": "RomanticSu", + "value": "62" + }, + { + "label": "R-15", + "value": "63" + }, + { + "label": "Racism", + "value": "208" + }, + { + "label": "R-18", + "value": "30" + }, + { + "label": "RuthlessPr", + "value": "286" + }, + { + "label": "RebirthedP", + "value": "1287" + }, + { + "label": "Regret", + "value": "3046" + }, + { + "label": "R15", + "value": "942" + }, + { + "label": "Regression", + "value": "3049" + }, + { + "label": "Rpe", + "value": "259" + }, + { + "label": "Rape", + "value": "13" + }, + { + "label": "ReverseHar", + "value": "581" + }, + { + "label": "Rivalry", + "value": "746" + }, + { + "label": "Religions", + "value": "622" + }, + { + "label": "relaxed", + "value": "1803" + }, + { + "label": "reincarnat", + "value": "1448" + }, + { + "label": "Resurrecti", + "value": "305" + }, + { + "label": "RomanceFan", + "value": "3047" + }, + { + "label": "RighteousP", + "value": "271" + }, + { + "label": "RaceChange", + "value": "580" + }, + { + "label": "Rarebloodl", + "value": "3149" + }, + { + "label": "RichProtag", + "value": "1205" + }, + { + "label": "Royalfamil", + "value": "3179" + }, + { + "label": "RichtoPoor", + "value": "315" + }, + { + "label": "Reborn", + "value": "836" + }, + { + "label": "ReverseRpe", + "value": "759" + }, + { + "label": "Rich", + "value": "1095" + }, + { + "label": "Richfamily", + "value": "1438" + }, + { + "label": "ReverseRap", + "value": "573" + }, + { + "label": "Roommates", + "value": "777" + }, + { + "label": "Restaurant", + "value": "788" + }, + { + "label": "Returntoth", + "value": "925" + }, + { + "label": "Relaxing", + "value": "987" + }, + { + "label": "runawayher", + "value": "1005" + }, + { + "label": "Reikyrecov", + "value": "1100" + }, + { + "label": "Reversal", + "value": "3209" + }, + { + "label": "reasoning", + "value": "3218" + }, + { + "label": "Reporters", + "value": "775" + }, + { + "label": "RichCharac", + "value": "876" + }, + { + "label": "RuthlessMc", + "value": "1141" + }, + { + "label": "Reunion", + "value": "1523" + }, + { + "label": "RimuruTemp", + "value": "1629" + }, + { + "label": "Rideawhale", + "value": "1858" + }, + { + "label": "riversande", + "value": "2048" + }, + { + "label": "Redemption", + "value": "3051" + }, + { + "label": "returnofth", + "value": "3286" + }, + { + "label": "Reincarnat", + "value": "14" + }, + { + "label": "Ruthless", + "value": "117" + }, + { + "label": "Reincarnat", + "value": "225" + }, + { + "label": "RapeVictim", + "value": "387" + }, + { + "label": "Reincarnat", + "value": "539" + }, + { + "label": "RpeVictimB", + "value": "717" + }, + { + "label": "Reincarnat", + "value": "749" + }, + { + "label": "RomanticSu", + "value": "754" + }, + { + "label": "Rebellion", + "value": "756" + }, + { + "label": "Returningf", + "value": "770" + }, + { + "label": "Reversible", + "value": "786" + }, + { + "label": "RedAlert2", + "value": "822" + }, + { + "label": "RapeVictim", + "value": "832" + }, + { + "label": "RomanticPr", + "value": "841" + }, + { + "label": "Role-Playi", + "value": "849" + }, + { + "label": "Reincarnat", + "value": "881" + }, + { + "label": "ReligousOr", + "value": "889" + }, + { + "label": "redalert", + "value": "927" + }, + { + "label": "Reversalof", + "value": "1048" + }, + { + "label": "Rejuvenati", + "value": "1055" + }, + { + "label": "ReluctantP", + "value": "1060" + }, + { + "label": "ReligiousO", + "value": "1101" + }, + { + "label": "Returntoth", + "value": "1187" + }, + { + "label": "Rebornprot", + "value": "1285" + }, + { + "label": "Rune", + "value": "1290" + }, + { + "label": "RookieProt", + "value": "1408" + }, + { + "label": "revenge", + "value": "1447" + }, + { + "label": "Reincarnat", + "value": "1481" + }, + { + "label": "Regressor", + "value": "1599" + }, + { + "label": "RWBY", + "value": "1722" + }, + { + "label": "Ras", + "value": "1730" + }, + { + "label": "Researcher", + "value": "1744" + }, + { + "label": "reincarnat", + "value": "1780" + }, + { + "label": "RankingLis", + "value": "1786" + }, + { + "label": "ReikiRecov", + "value": "1807" + }, + { + "label": "Russian", + "value": "1808" + }, + { + "label": "RinYueqing", + "value": "1831" + }, + { + "label": "runawaycit", + "value": "1852" + }, + { + "label": "runawayant", + "value": "1881" + }, + { + "label": "RoyalSabur", + "value": "1995" + }, + { + "label": "reversesmo", + "value": "2084" + }, + { + "label": "Rapeseedra", + "value": "2198" + }, + { + "label": "Ruoshuithr", + "value": "2284" + }, + { + "label": "rainandsno", + "value": "2293" + }, + { + "label": "reallyking", + "value": "2317" + }, + { + "label": "restaurant", + "value": "2328" + }, + { + "label": "recreation", + "value": "2363" + }, + { + "label": "Residencen", + "value": "2439" + }, + { + "label": "richeveryy", + "value": "2568" + }, + { + "label": "RabbitToot", + "value": "2601" + }, + { + "label": "Roon", + "value": "2609" + }, + { + "label": "raisedache", + "value": "2655" + }, + { + "label": "Raiseaghos", + "value": "2672" + }, + { + "label": "Rotaryhotp", + "value": "2692" + }, + { + "label": "rainboweig", + "value": "2745" + }, + { + "label": "Resurrecti", + "value": "2758" + }, + { + "label": "rainydaywi", + "value": "2849" + }, + { + "label": "Realmmonst", + "value": "2873" + }, + { + "label": "Residentev", + "value": "2926" + }, + { + "label": "Races", + "value": "3000" + }, + { + "label": "RiseofGras", + "value": "3011" + }, + { + "label": "Reiki", + "value": "3063" + }, + { + "label": "RPG", + "value": "3080" + }, + { + "label": "Reincarnat", + "value": "3120" + }, + { + "label": "Reverse", + "value": "3122" + }, + { + "label": "Righteous", + "value": "3195" + }, + { + "label": "Return", + "value": "3215" + }, + { + "label": "reality", + "value": "3276" + }, + { + "label": "日常", + "value": "3289" + }, + { + "label": "RanchFarmi", + "value": "3301" + }, + { + "label": "System", + "value": "168" + }, + { + "label": "SystemAdmi", + "value": "169" + }, + { + "label": "SecondChan", + "value": "41" + }, + { + "label": "SwordAndMa", + "value": "287" + }, + { + "label": "Showbiz", + "value": "103" + }, + { + "label": "SpecialAbi", + "value": "334" + }, + { + "label": "StrongtoSt", + "value": "558" + }, + { + "label": "SlowRomanc", + "value": "361" + }, + { + "label": "Sign-InChe", + "value": "1288" + }, + { + "label": "Survival", + "value": "291" + }, + { + "label": "Superpower", + "value": "408" + }, + { + "label": "SliceofLif", + "value": "906" + }, + { + "label": "StrongLove", + "value": "264" + }, + { + "label": "ShamelessP", + "value": "87" + }, + { + "label": "SuperTechn", + "value": "1289" + }, + { + "label": "SemeProtag", + "value": "588" + }, + { + "label": "StrongProt", + "value": "951" + }, + { + "label": "Strongfrom", + "value": "362" + }, + { + "label": "SurvivalGa", + "value": "693" + }, + { + "label": "SwordWield", + "value": "272" + }, + { + "label": "Slaves", + "value": "74" + }, + { + "label": "ShoujoAi", + "value": "1074" + }, + { + "label": "Soccer", + "value": "155" + }, + { + "label": "SecretIden", + "value": "118" + }, + { + "label": "SlowGrowth", + "value": "295" + }, + { + "label": "Spirits", + "value": "663" + }, + { + "label": "SmartCoupl", + "value": "263" + }, + { + "label": "StrongBack", + "value": "1376" + }, + { + "label": "SuddenWeal", + "value": "200" + }, + { + "label": "SummoningM", + "value": "579" + }, + { + "label": "Serious", + "value": "986" + }, + { + "label": "Swordsman", + "value": "99" + }, + { + "label": "Space", + "value": "884" + }, + { + "label": "Singers", + "value": "500" + }, + { + "label": "Strategist", + "value": "731" + }, + { + "label": "SweetText", + "value": "896" + }, + { + "label": "Shuangwen", + "value": "1251" + }, + { + "label": "Sports", + "value": "1377" + }, + { + "label": "StoreOwner", + "value": "331" + }, + { + "label": "SchoolLife", + "value": "654" + }, + { + "label": "SkillAssim", + "value": "352" + }, + { + "label": "Scientists", + "value": "427" + }, + { + "label": "StrategicB", + "value": "687" + }, + { + "label": "Supernatur", + "value": "664" + }, + { + "label": "SecretOrga", + "value": "1183" + }, + { + "label": "Sects", + "value": "86" + }, + { + "label": "SuddenStre", + "value": "416" + }, + { + "label": "Smut", + "value": "3145" + }, + { + "label": "SpaceOpera", + "value": "101" + }, + { + "label": "SicklyChar", + "value": "474" + }, + { + "label": "Spaceship", + "value": "632" + }, + { + "label": "Souls", + "value": "642" + }, + { + "label": "Sci-fi", + "value": "898" + }, + { + "label": "SentientSk", + "value": "1365" + }, + { + "label": "SectDevelo", + "value": "507" + }, + { + "label": "Soldiers", + "value": "612" + }, + { + "label": "SisterComp", + "value": "347" + }, + { + "label": "SinglePare", + "value": "415" + }, + { + "label": "SecretOrga", + "value": "755" + }, + { + "label": "Sciencefic", + "value": "1706" + }, + { + "label": "Salvation", + "value": "3060" + }, + { + "label": "SummonedHe", + "value": "64" + }, + { + "label": "SlowLife", + "value": "953" + }, + { + "label": "SlowCultiv", + "value": "1079" + }, + { + "label": "Summons", + "value": "1512" + }, + { + "label": "SoulPower", + "value": "136" + }, + { + "label": "Shoujo-AiS", + "value": "666" + }, + { + "label": "Secrets", + "value": "675" + }, + { + "label": "Siblings", + "value": "442" + }, + { + "label": "SexualAbus", + "value": "569" + }, + { + "label": "Shounen-Ai", + "value": "598" + }, + { + "label": "Sweet", + "value": "1203" + }, + { + "label": "Summoner", + "value": "907" + }, + { + "label": "Sweetlove", + "value": "3156" + }, + { + "label": "SlaveProta", + "value": "627" + }, + { + "label": "StraightUk", + "value": "700" + }, + { + "label": "Saints", + "value": "741" + }, + { + "label": "ShyCharact", + "value": "761" + }, + { + "label": "SkillBooks", + "value": "766" + }, + { + "label": "SkillCreat", + "value": "767" + }, + { + "label": "Status", + "value": "965" + }, + { + "label": "smartprota", + "value": "1261" + }, + { + "label": "ShortStory", + "value": "641" + }, + { + "label": "SpiritUser", + "value": "798" + }, + { + "label": "SmartMC", + "value": "905" + }, + { + "label": "Schemesand", + "value": "1091" + }, + { + "label": "Simulator", + "value": "1219" + }, + { + "label": "system", + "value": "1456" + }, + { + "label": "SystemFlow", + "value": "1683" + }, + { + "label": "SerialKill", + "value": "218" + }, + { + "label": "StockholmS", + "value": "620" + }, + { + "label": "Shapeshift", + "value": "688" + }, + { + "label": "SavingtheW", + "value": "760" + }, + { + "label": "SealedPowe", + "value": "771" + }, + { + "label": "StrongMC", + "value": "854" + }, + { + "label": "Shapeshift", + "value": "1147" + }, + { + "label": "SuperHeroe", + "value": "1175" + }, + { + "label": "SigninChec", + "value": "1384" + }, + { + "label": "summon", + "value": "161" + }, + { + "label": "SlaveHarem", + "value": "209" + }, + { + "label": "StubbornPr", + "value": "216" + }, + { + "label": "SelfishPro", + "value": "508" + }, + { + "label": "Servants", + "value": "571" + }, + { + "label": "SexSlaves", + "value": "574" + }, + { + "label": "Shota", + "value": "584" + }, + { + "label": "SecretCrus", + "value": "600" + }, + { + "label": "SecretiveP", + "value": "635" + }, + { + "label": "SelflessPr", + "value": "640" + }, + { + "label": "StoicChara", + "value": "669" + }, + { + "label": "StrongFema", + "value": "893" + }, + { + "label": "Summoning", + "value": "980" + }, + { + "label": "Suicides", + "value": "1007" + }, + { + "label": "Slice-of-l", + "value": "1307" + }, + { + "label": "signin", + "value": "1346" + }, + { + "label": "StrongCoup", + "value": "1429" + }, + { + "label": "SingleHero", + "value": "1605" + }, + { + "label": "straightma", + "value": "2933" + }, + { + "label": "Singlefema", + "value": "3193" + }, + { + "label": "SadisticCh", + "value": "556" + }, + { + "label": "SpecialAbi", + "value": "668" + }, + { + "label": "StraightSe", + "value": "699" + }, + { + "label": "Seduction", + "value": "732" + }, + { + "label": "SpearWield", + "value": "764" + }, + { + "label": "SpiritAdvi", + "value": "800" + }, + { + "label": "Sign-in", + "value": "900" + }, + { + "label": "star", + "value": "934" + }, + { + "label": "Skills", + "value": "1006" + }, + { + "label": "Supernatur", + "value": "1015" + }, + { + "label": "SAT", + "value": "1034" + }, + { + "label": "Singer", + "value": "1065" + }, + { + "label": "SCP", + "value": "1182" + }, + { + "label": "Superstar", + "value": "1190" + }, + { + "label": "Suspense", + "value": "1256" + }, + { + "label": "systemowne", + "value": "1286" + }, + { + "label": "Superpower", + "value": "1316" + }, + { + "label": "Slave", + "value": "1500" + }, + { + "label": "Shounen", + "value": "1569" + }, + { + "label": "Strategy", + "value": "3012" + }, + { + "label": "Self-disci", + "value": "3086" + }, + { + "label": "Scary", + "value": "3188" + }, + { + "label": "science", + "value": "129" + }, + { + "label": "SocialOutc", + "value": "736" + }, + { + "label": "Spies", + "value": "751" + }, + { + "label": "SpatialMan", + "value": "795" + }, + { + "label": "Shotacon", + "value": "796" + }, + { + "label": "Succubus", + "value": "806" + }, + { + "label": "SpecialLik", + "value": "909" + }, + { + "label": "SxFriends", + "value": "945" + }, + { + "label": "strong", + "value": "959" + }, + { + "label": "Skill", + "value": "979" + }, + { + "label": "Samurai", + "value": "1020" + }, + { + "label": "SxSlaves", + "value": "1049" + }, + { + "label": "Saves", + "value": "1050" + }, + { + "label": "SaikiK", + "value": "1107" + }, + { + "label": "SaintSeiya", + "value": "1132" + }, + { + "label": "StrongLove", + "value": "1270" + }, + { + "label": "SpecialFor", + "value": "1381" + }, + { + "label": "Son-in-law", + "value": "1498" + }, + { + "label": "ShouProtag", + "value": "1504" + }, + { + "label": "Sea", + "value": "1606" + }, + { + "label": "Simulation", + "value": "1685" + }, + { + "label": "SecondChan", + "value": "1705" + }, + { + "label": "secretary", + "value": "1776" + }, + { + "label": "smallninel", + "value": "1938" + }, + { + "label": "Swordgod", + "value": "2645" + }, + { + "label": "Satire", + "value": "2996" + }, + { + "label": "stallion", + "value": "3075" + }, + { + "label": "Secretive", + "value": "3158" + }, + { + "label": "Seductive", + "value": "3194" + }, + { + "label": "社会", + "value": "3229" + }, + { + "label": "Student", + "value": "3270" + }, + { + "label": "Scavengers", + "value": "84" + }, + { + "label": "SecondChan", + "value": "123" + }, + { + "label": "SchemesAnd", + "value": "260" + }, + { + "label": "Schizophre", + "value": "261" + }, + { + "label": "Sharp-tong", + "value": "378" + }, + { + "label": "SiblingsNo", + "value": "429" + }, + { + "label": "Strength-b", + "value": "527" + }, + { + "label": "SexualCult", + "value": "638" + }, + { + "label": "SeeingThin", + "value": "662" + }, + { + "label": "Student-Te", + "value": "714" + }, + { + "label": "Sentimenta", + "value": "735" + }, + { + "label": "SxualAbuse", + "value": "791" + }, + { + "label": "SiblingRiv", + "value": "804" + }, + { + "label": "SevenDeadl", + "value": "808" + }, + { + "label": "Sibling&am", + "value": "811" + }, + { + "label": "SlaveSyste", + "value": "833" + }, + { + "label": "StrongSubo", + "value": "843" + }, + { + "label": "SuperSemin", + "value": "844" + }, + { + "label": "Streamer", + "value": "877" + }, + { + "label": "StrongPowe", + "value": "882" + }, + { + "label": "SweetYaoi", + "value": "916" + }, + { + "label": "Spy", + "value": "932" + }, + { + "label": "shokugekin", + "value": "939" + }, + { + "label": "SxualCulti", + "value": "947" + }, + { + "label": "SaveHarem", + "value": "948" + }, + { + "label": "Senpai-Kou", + "value": "960" + }, + { + "label": "SharingABo", + "value": "981" + }, + { + "label": "Strongests", + "value": "993" + }, + { + "label": "Strongprod", + "value": "1013" + }, + { + "label": "strongcomb", + "value": "1014" + }, + { + "label": "Sometimesa", + "value": "1019" + }, + { + "label": "SevenVirtu", + "value": "1044" + }, + { + "label": "SentientOb", + "value": "1061" + }, + { + "label": "StrongOpFe", + "value": "1108" + }, + { + "label": "SaltedFish", + "value": "1136" + }, + { + "label": "Si-fi", + "value": "1142" + }, + { + "label": "SeaExplora", + "value": "1160" + }, + { + "label": "Starcraft", + "value": "1165" + }, + { + "label": "SonOfAGodP", + "value": "1177" + }, + { + "label": "SpecialLov", + "value": "1208" + }, + { + "label": "SameSexMar", + "value": "1215" + }, + { + "label": "Sequel", + "value": "1225" + }, + { + "label": "StrongFema", + "value": "1244" + }, + { + "label": "SpiritualR", + "value": "1265" + }, + { + "label": "SpiritAnal", + "value": "1283" + }, + { + "label": "School-lif", + "value": "1319" + }, + { + "label": "SlightlySu", + "value": "1322" + }, + { + "label": "SchemingPr", + "value": "1335" + }, + { + "label": "SpiritualQ", + "value": "1361" + }, + { + "label": "SwordArtOn", + "value": "1373" + }, + { + "label": "SystemTran", + "value": "1383" + }, + { + "label": "SwallowedS", + "value": "1392" + }, + { + "label": "SkillSteal", + "value": "1403" + }, + { + "label": "SaikiK.", + "value": "1431" + }, + { + "label": "secondchan", + "value": "1457" + }, + { + "label": "sport", + "value": "1479" + }, + { + "label": "Schemes", + "value": "1497" + }, + { + "label": "SystemTran", + "value": "1505" + }, + { + "label": "sweetroman", + "value": "1524" + }, + { + "label": "Sysetm", + "value": "1537" + }, + { + "label": "Shelter", + "value": "1542" + }, + { + "label": "StrongestP", + "value": "1556" + }, + { + "label": "Scientist", + "value": "1560" + }, + { + "label": "Supportive", + "value": "1561" + }, + { + "label": "SpecialAbi", + "value": "1594" + }, + { + "label": "Saint", + "value": "1597" + }, + { + "label": "SlapstickC", + "value": "1602" + }, + { + "label": "SecretRela", + "value": "1659" + }, + { + "label": "Stepmother", + "value": "1664" + }, + { + "label": "SchoolSett", + "value": "1729" + }, + { + "label": "shounenai", + "value": "1762" + }, + { + "label": "Siscon", + "value": "1769" + }, + { + "label": "Sailing", + "value": "1777" + }, + { + "label": "schemeandc", + "value": "1785" + }, + { + "label": "strongfema", + "value": "1789" + }, + { + "label": "submissive", + "value": "1802" + }, + { + "label": "suicidalpr", + "value": "1815" + }, + { + "label": "smokeinthe", + "value": "1830" + }, + { + "label": "SmokeCloud", + "value": "1839" + }, + { + "label": "Shallowsea", + "value": "1848" + }, + { + "label": "ServantofZ", + "value": "1856" + }, + { + "label": "shrimpinth", + "value": "1857" + }, + { + "label": "Scalesofth", + "value": "1866" + }, + { + "label": "shudder", + "value": "1868" + }, + { + "label": "sweetjelly", + "value": "1873" + }, + { + "label": "Swordblood", + "value": "1882" + }, + { + "label": "sadsword", + "value": "1888" + }, + { + "label": "Shouldhand", + "value": "1889" + }, + { + "label": "SaltedFish", + "value": "1891" + }, + { + "label": "Sterile", + "value": "1901" + }, + { + "label": "sundaysun", + "value": "1907" + }, + { + "label": "仕辰", + "value": "1914" + }, + { + "label": "SuYechen", + "value": "1918" + }, + { + "label": "sevenpigeo", + "value": "1921" + }, + { + "label": "summertrip", + "value": "1923" + }, + { + "label": "SuShaoqing", + "value": "1925" + }, + { + "label": "summertree", + "value": "1928" + }, + { + "label": "SwordImmor", + "value": "1929" + }, + { + "label": "sillycatse", + "value": "1946" + }, + { + "label": "six-twochi", + "value": "1953" + }, + { + "label": "Sadreminde", + "value": "1972" + }, + { + "label": "sleepslate", + "value": "2011" + }, + { + "label": "Scourge", + "value": "2013" + }, + { + "label": "ShenLuo", + "value": "2032" + }, + { + "label": "sleepingsa", + "value": "2041" + }, + { + "label": "SuperGodGr", + "value": "2058" + }, + { + "label": "stupidfox", + "value": "2061" + }, + { + "label": "snorkeling", + "value": "2077" + }, + { + "label": "spendthewo", + "value": "2085" + }, + { + "label": "showstory", + "value": "2090" + }, + { + "label": "Sixty-six", + "value": "2091" + }, + { + "label": "silentkill", + "value": "2095" + }, + { + "label": "ShenhuoxoR", + "value": "2111" + }, + { + "label": "sadsadness", + "value": "2125" + }, + { + "label": "sandrivere", + "value": "2150" + }, + { + "label": "startwriti", + "value": "2161" + }, + { + "label": "Siheyuanfl", + "value": "2214" + }, + { + "label": "SakuraMoon", + "value": "2246" + }, + { + "label": "Sevengener", + "value": "2248" + }, + { + "label": "Sword丨Lea", + "value": "2265" + }, + { + "label": "SiheyuanGo", + "value": "2285" + }, + { + "label": "SoulChef", + "value": "2287" + }, + { + "label": "SuZiyouyou", + "value": "2288" + }, + { + "label": "startofthe", + "value": "2297" + }, + { + "label": "Sweetandso", + "value": "2299" + }, + { + "label": "SuperPiran", + "value": "2312" + }, + { + "label": "SiheyuanDe", + "value": "2313" + }, + { + "label": "SystemNo.3", + "value": "2315" + }, + { + "label": "SaltedFish", + "value": "2325" + }, + { + "label": "shadowghos", + "value": "2341" + }, + { + "label": "scumteache", + "value": "2349" + }, + { + "label": "SkinButler", + "value": "2365" + }, + { + "label": "StinkBeanS", + "value": "2376" + }, + { + "label": "ShuYuChenX", + "value": "2377" + }, + { + "label": "Silencehim", + "value": "2383" + }, + { + "label": "specialwar", + "value": "2394" + }, + { + "label": "StudentUni", + "value": "2395" + }, + { + "label": "SillyColum", + "value": "2401" + }, + { + "label": "森罗", + "value": "2404" + }, + { + "label": "signinsalt", + "value": "2409" + }, + { + "label": "self-disci", + "value": "2414" + }, + { + "label": "Simpleone", + "value": "2417" + }, + { + "label": "stardarkni", + "value": "2418" + }, + { + "label": "SuWei", + "value": "2423" + }, + { + "label": "sisterisbe", + "value": "2424" + }, + { + "label": "Supernatur", + "value": "2428" + }, + { + "label": "SanmitheGr", + "value": "2437" + }, + { + "label": "Saltedfish", + "value": "2443" + }, + { + "label": "SoulCelest", + "value": "2447" + }, + { + "label": "soulanddre", + "value": "2461" + }, + { + "label": "secondpira", + "value": "2467" + }, + { + "label": "SixPathsof", + "value": "2489" + }, + { + "label": "stevec", + "value": "2505" + }, + { + "label": "sleeplesst", + "value": "2518" + }, + { + "label": "Secretobse", + "value": "2524" + }, + { + "label": "StewedChic", + "value": "2527" + }, + { + "label": "SuperGodNo", + "value": "2534" + }, + { + "label": "Superfire", + "value": "2545" + }, + { + "label": "Stomachhur", + "value": "2548" + }, + { + "label": "SongoftheG", + "value": "2560" + }, + { + "label": "stablefort", + "value": "2567" + }, + { + "label": "stonemored", + "value": "2572" + }, + { + "label": "soulmemory", + "value": "2573" + }, + { + "label": "Straightme", + "value": "2584" + }, + { + "label": "ShenJin", + "value": "2599" + }, + { + "label": "sistercook", + "value": "2606" + }, + { + "label": "Smallmushr", + "value": "2607" + }, + { + "label": "ShenhaoMec", + "value": "2619" + }, + { + "label": "singlesalt", + "value": "2628" + }, + { + "label": "summernow", + "value": "2641" + }, + { + "label": "swordrepai", + "value": "2680" + }, + { + "label": "Smokebambo", + "value": "2702" + }, + { + "label": "Sakurajima", + "value": "2706" + }, + { + "label": "Sencha", + "value": "2724" + }, + { + "label": "starfish", + "value": "2729" + }, + { + "label": "swearnotto", + "value": "2732" + }, + { + "label": "SaltedFish", + "value": "2744" + }, + { + "label": "Swimmingfi", + "value": "2748" + }, + { + "label": "speechless", + "value": "2750" + }, + { + "label": "SacrificeX", + "value": "2781" + }, + { + "label": "sopoor", + "value": "2790" + }, + { + "label": "SwingingDe", + "value": "2794" + }, + { + "label": "supernovab", + "value": "2797" + }, + { + "label": "softorange", + "value": "2812" + }, + { + "label": "sunsetover", + "value": "2815" + }, + { + "label": "streamerbl", + "value": "2819" + }, + { + "label": "SouthKefei", + "value": "2832" + }, + { + "label": "shadowfall", + "value": "2860" + }, + { + "label": "stopatfirs", + "value": "2862" + }, + { + "label": "silver", + "value": "2871" + }, + { + "label": "Science-fi", + "value": "2912" + }, + { + "label": "Stand", + "value": "2923" + }, + { + "label": "Show-biz", + "value": "2928" + }, + { + "label": "SonInLaw", + "value": "2958" + }, + { + "label": "Shadowless", + "value": "2971" + }, + { + "label": "Superman", + "value": "2975" + }, + { + "label": "Senbeiboy", + "value": "2981" + }, + { + "label": "Sugary", + "value": "2991" + }, + { + "label": "SoftSci-fi", + "value": "3004" + }, + { + "label": "Starwars", + "value": "3007" + }, + { + "label": "Sect", + "value": "3018" + }, + { + "label": "SelfDiscip", + "value": "3019" + }, + { + "label": "Smart", + "value": "3022" + }, + { + "label": "steamponk", + "value": "3024" + }, + { + "label": "systemmale", + "value": "3035" + }, + { + "label": "Softyander", + "value": "3057" + }, + { + "label": "Slvery", + "value": "3095" + }, + { + "label": "strongwoma", + "value": "3110" + }, + { + "label": "skycity", + "value": "3113" + }, + { + "label": "specialpow", + "value": "3136" + }, + { + "label": "Strongfl", + "value": "3154" + }, + { + "label": "SuperAbili", + "value": "3159" + }, + { + "label": "Stepmom", + "value": "3173" + }, + { + "label": "Studenttea", + "value": "3185" + }, + { + "label": "Supporting", + "value": "3210" + }, + { + "label": "Sinology", + "value": "3220" + }, + { + "label": "strongfema", + "value": "3237" + }, + { + "label": "sweetpet", + "value": "3257" + }, + { + "label": "Shenhao", + "value": "3285" + }, + { + "label": "Sectbuildi", + "value": "3295" + }, + { + "label": "Sciencefic", + "value": "3297" + }, + { + "label": "Transmigra", + "value": "57" + }, + { + "label": "TimeTravel", + "value": "48" + }, + { + "label": "Talents", + "value": "1366" + }, + { + "label": "TragicPast", + "value": "533" + }, + { + "label": "Tragedy", + "value": "857" + }, + { + "label": "TimeSkip", + "value": "27" + }, + { + "label": "Tsundere", + "value": "38" + }, + { + "label": "Technologi", + "value": "302" + }, + { + "label": "Teamwork", + "value": "181" + }, + { + "label": "Twins", + "value": "441" + }, + { + "label": "Thestronga", + "value": "3174" + }, + { + "label": "TwistedPer", + "value": "182" + }, + { + "label": "Thriller", + "value": "138" + }, + { + "label": "Teachers", + "value": "665" + }, + { + "label": "TimeLoop", + "value": "47" + }, + { + "label": "TimeManipu", + "value": "543" + }, + { + "label": "Transplant", + "value": "226" + }, + { + "label": "Thieves", + "value": "705" + }, + { + "label": "Threesome", + "value": "210" + }, + { + "label": "TomboyishF", + "value": "348" + }, + { + "label": "TribalSoci", + "value": "803" + }, + { + "label": "TS", + "value": "3092" + }, + { + "label": "Trap", + "value": "639" + }, + { + "label": "Talent", + "value": "659" + }, + { + "label": "Torture", + "value": "763" + }, + { + "label": "Temple", + "value": "1009" + }, + { + "label": "Toriko", + "value": "1390" + }, + { + "label": "Transmigat", + "value": "1652" + }, + { + "label": "TopMC", + "value": "1695" + }, + { + "label": "twilight", + "value": "2006" + }, + { + "label": "Tennis", + "value": "671" + }, + { + "label": "Traverse", + "value": "866" + }, + { + "label": "Trickster", + "value": "995" + }, + { + "label": "Technology", + "value": "1086" + }, + { + "label": "Transmigra", + "value": "1146" + }, + { + "label": "TimeParado", + "value": "1184" + }, + { + "label": "TerritoryC", + "value": "1382" + }, + { + "label": "Transmigra", + "value": "1548" + }, + { + "label": "ThaiNovel", + "value": "1714" + }, + { + "label": "travel", + "value": "3114" + }, + { + "label": "teacher", + "value": "130" + }, + { + "label": "Titans", + "value": "160" + }, + { + "label": "TerminalIl", + "value": "440" + }, + { + "label": "Terrorists", + "value": "762" + }, + { + "label": "TreasureHu", + "value": "913" + }, + { + "label": "Tactics", + "value": "1028" + }, + { + "label": "Technology", + "value": "1166" + }, + { + "label": "Transmigra", + "value": "1254" + }, + { + "label": "team", + "value": "1401" + }, + { + "label": "Taoist", + "value": "1509" + }, + { + "label": "Tensura", + "value": "1630" + }, + { + "label": "TypeMoon", + "value": "1631" + }, + { + "label": "Tasker", + "value": "1666" + }, + { + "label": "TangJichen", + "value": "1886" + }, + { + "label": "Time-Trave", + "value": "1979" + }, + { + "label": "TsukibaAki", + "value": "2178" + }, + { + "label": "Twisted", + "value": "3169" + }, + { + "label": "Transporte", + "value": "21" + }, + { + "label": "Transporte", + "value": "88" + }, + { + "label": "Tsuru", + "value": "158" + }, + { + "label": "Transporte", + "value": "279" + }, + { + "label": "Transporte", + "value": "491" + }, + { + "label": "Transforma", + "value": "606" + }, + { + "label": "TableTenni", + "value": "670" + }, + { + "label": "Tranformer", + "value": "848" + }, + { + "label": "Transworld", + "value": "967" + }, + { + "label": "Theheroist", + "value": "972" + }, + { + "label": "Toys", + "value": "1036" + }, + { + "label": "TeamManage", + "value": "1066" + }, + { + "label": "Television", + "value": "1071" + }, + { + "label": "TreasureHu", + "value": "1084" + }, + { + "label": "Transmigra", + "value": "1088" + }, + { + "label": "Transmigra", + "value": "1092" + }, + { + "label": "TravelingT", + "value": "1125" + }, + { + "label": "Transmigra", + "value": "1161" + }, + { + "label": "Transmigra", + "value": "1164" + }, + { + "label": "Talismans", + "value": "1221" + }, + { + "label": "Twinbabies", + "value": "1222" + }, + { + "label": "TowerDefen", + "value": "1223" + }, + { + "label": "TheManInTh", + "value": "1271" + }, + { + "label": "Transportt", + "value": "1277" + }, + { + "label": "TreasureHu", + "value": "1284" + }, + { + "label": "TransportI", + "value": "1351" + }, + { + "label": "TeacherDis", + "value": "1397" + }, + { + "label": "TeacherMC", + "value": "1398" + }, + { + "label": "ThreeKingd", + "value": "1406" + }, + { + "label": "transmigra", + "value": "1469" + }, + { + "label": "theevernon", + "value": "1473" + }, + { + "label": "tv", + "value": "1478" + }, + { + "label": "Tokyo", + "value": "1483" + }, + { + "label": "TimeandSpa", + "value": "1536" + }, + { + "label": "TalentShow", + "value": "1557" + }, + { + "label": "Trnasmigra", + "value": "1588" + }, + { + "label": "Traveling", + "value": "1607" + }, + { + "label": "Tramsmigra", + "value": "1637" + }, + { + "label": "traveller", + "value": "1663" + }, + { + "label": "TheGamer", + "value": "1721" + }, + { + "label": "TheAsteris", + "value": "1726" + }, + { + "label": "Tailsman", + "value": "1756" + }, + { + "label": "TsundereLo", + "value": "1766" + }, + { + "label": "TerritoryM", + "value": "1768" + }, + { + "label": "TokyoGhoul", + "value": "1782" + }, + { + "label": "Tyrant", + "value": "1804" + }, + { + "label": "Tianbang78", + "value": "1825" + }, + { + "label": "Theflowero", + "value": "1850" + }, + { + "label": "Thesunsett", + "value": "1851" + }, + { + "label": "Threedaysa", + "value": "1894" + }, + { + "label": "Twilightis", + "value": "1896" + }, + { + "label": "TheGospelo", + "value": "1899" + }, + { + "label": "TheThreeKi", + "value": "1910" + }, + { + "label": "TingFengZh", + "value": "1932" + }, + { + "label": "tomorrowwi", + "value": "1936" + }, + { + "label": "Three-flav", + "value": "1951" + }, + { + "label": "Thelightof", + "value": "1955" + }, + { + "label": "TaurenIron", + "value": "1968" + }, + { + "label": "Tenthousan", + "value": "1974" + }, + { + "label": "TempleThir", + "value": "1988" + }, + { + "label": "ThreshingG", + "value": "1991" + }, + { + "label": "Technology", + "value": "2002" + }, + { + "label": "thegodofde", + "value": "2018" + }, + { + "label": "Thunderous", + "value": "2021" + }, + { + "label": "ToneMasaya", + "value": "2047" + }, + { + "label": "Thebiggest", + "value": "2052" + }, + { + "label": "Thebigdevi", + "value": "2065" + }, + { + "label": "TopoftheCl", + "value": "2070" + }, + { + "label": "thisyear", + "value": "2071" + }, + { + "label": "Thenewbact", + "value": "2100" + }, + { + "label": "ThreeLives", + "value": "2103" + }, + { + "label": "thewindisb", + "value": "2123" + }, + { + "label": "铁帅", + "value": "2130" + }, + { + "label": "TopoftheCl", + "value": "2141" + }, + { + "label": "Thestronge", + "value": "2143" + }, + { + "label": "TeckTyrann", + "value": "2144" + }, + { + "label": "Theoceando", + "value": "2154" + }, + { + "label": "therearefi", + "value": "2182" + }, + { + "label": "Tianbangth", + "value": "2184" + }, + { + "label": "TheGodfath", + "value": "2200" + }, + { + "label": "TenCommand", + "value": "2201" + }, + { + "label": "takestock", + "value": "2209" + }, + { + "label": "tobacco", + "value": "2224" + }, + { + "label": "Thinkingof", + "value": "2229" + }, + { + "label": "threelittl", + "value": "2239" + }, + { + "label": "Theworld&a", + "value": "2242" + }, + { + "label": "TombRaider", + "value": "2252" + }, + { + "label": "TangShaoqi", + "value": "2259" + }, + { + "label": "Tianbangol", + "value": "2262" + }, + { + "label": "Theancesto", + "value": "2298" + }, + { + "label": "Thequeenis", + "value": "2339" + }, + { + "label": "Thetruegod", + "value": "2342" + }, + { + "label": "TianYiding", + "value": "2343" + }, + { + "label": "TianbangYa", + "value": "2352" + }, + { + "label": "Thirty-two", + "value": "2357" + }, + { + "label": "Tianshitak", + "value": "2368" + }, + { + "label": "TombRaider", + "value": "2386" + }, + { + "label": "Theoldfive", + "value": "2389" + }, + { + "label": "Two-dimens", + "value": "2413" + }, + { + "label": "TwentyFame", + "value": "2416" + }, + { + "label": "Thelistisi", + "value": "2419" + }, + { + "label": "threeteeth", + "value": "2420" + }, + { + "label": "ThousandTe", + "value": "2442" + }, + { + "label": "Theashesar", + "value": "2455" + }, + { + "label": "TopoftheFo", + "value": "2456" + }, + { + "label": "takeoverth", + "value": "2457" + }, + { + "label": "TombRaider", + "value": "2465" + }, + { + "label": "Two-dimens", + "value": "2468" + }, + { + "label": "Teemotofly", + "value": "2481" + }, + { + "label": "TombRaider", + "value": "2483" + }, + { + "label": "TrumanLive", + "value": "2495" + }, + { + "label": "Takeaplane", + "value": "2499" + }, + { + "label": "Tsunderesc", + "value": "2501" + }, + { + "label": "Thecatisgo", + "value": "2517" + }, + { + "label": "Thefishmar", + "value": "2538" + }, + { + "label": "Thousandso", + "value": "2557" + }, + { + "label": "ThreeLives", + "value": "2558" + }, + { + "label": "Tigerteeth", + "value": "2583" + }, + { + "label": "TroubledWo", + "value": "2589" + }, + { + "label": "Thepowerof", + "value": "2592" + }, + { + "label": "TimeKingJO", + "value": "2615" + }, + { + "label": "Thankyoufo", + "value": "2621" + }, + { + "label": "TianbangHu", + "value": "2661" + }, + { + "label": "Two-dimens", + "value": "2673" + }, + { + "label": "TenThousan", + "value": "2684" + }, + { + "label": "takeoffboy", + "value": "2690" + }, + { + "label": "Twistbroth", + "value": "2698" + }, + { + "label": "towashthed", + "value": "2719" + }, + { + "label": "Thegloryof", + "value": "2728" + }, + { + "label": "Twopoundso", + "value": "2734" + }, + { + "label": "Today&0", + "value": "2763" + }, + { + "label": "ThreeDotIn", + "value": "2775" + }, + { + "label": "Twopeopleb", + "value": "2778" + }, + { + "label": "Toilet", + "value": "2779" + }, + { + "label": "TopoftheCl", + "value": "2801" + }, + { + "label": "Tianbanggr", + "value": "2803" + }, + { + "label": "Thelistdep", + "value": "2807" + }, + { + "label": "Thewayofth", + "value": "2824" + }, + { + "label": "twingods", + "value": "2828" + }, + { + "label": "twilightdr", + "value": "2830" + }, + { + "label": "Thisissure", + "value": "2833" + }, + { + "label": "TeenageXia", + "value": "2846" + }, + { + "label": "treeofenli", + "value": "2867" + }, + { + "label": "Thetopofth", + "value": "2903" + }, + { + "label": "TangThirty", + "value": "2904" + }, + { + "label": "Trade", + "value": "2927" + }, + { + "label": "TrueorFake", + "value": "2955" + }, + { + "label": "TianbangDi", + "value": "2983" + }, + { + "label": "teacher-st", + "value": "3032" + }, + { + "label": "two-wayred", + "value": "3036" + }, + { + "label": "Timelimit", + "value": "3048" + }, + { + "label": "Turtle", + "value": "3070" + }, + { + "label": "teachermal", + "value": "3137" + }, + { + "label": "Terrori", + "value": "3160" + }, + { + "label": "TheMainCha", + "value": "3166" + }, + { + "label": "TheDevil", + "value": "3167" + }, + { + "label": "Teen", + "value": "3189" + }, + { + "label": "Talkshow", + "value": "3240" + }, + { + "label": "Thereareal", + "value": "3248" + }, + { + "label": "Thereisasu", + "value": "3250" + }, + { + "label": "Taoistprie", + "value": "3291" + }, + { + "label": "Urban", + "value": "144" + }, + { + "label": "UrbanLife", + "value": "860" + }, + { + "label": "UglytoBeau", + "value": "298" + }, + { + "label": "UnluckyPro", + "value": "379" + }, + { + "label": "Unrequited", + "value": "430" + }, + { + "label": "Unconditio", + "value": "728" + }, + { + "label": "Unprincipl", + "value": "3177" + }, + { + "label": "urbanroman", + "value": "3282" + }, + { + "label": "Unlimitedf", + "value": "955" + }, + { + "label": "UniqueWeap", + "value": "354" + }, + { + "label": "Unreliable", + "value": "613" + }, + { + "label": "UniqueWeap", + "value": "778" + }, + { + "label": "UglyProtag", + "value": "793" + }, + { + "label": "Undead", + "value": "3105" + }, + { + "label": "upgrade", + "value": "3224" + }, + { + "label": "UniqueCult", + "value": "211" + }, + { + "label": "Underestim", + "value": "267" + }, + { + "label": "unconsciou", + "value": "1026" + }, + { + "label": "unexpected", + "value": "1444" + }, + { + "label": "Urbanyouth", + "value": "1770" + }, + { + "label": "Undocument", + "value": "1841" + }, + { + "label": "Unknowntea", + "value": "1871" + }, + { + "label": "UrbanDatan", + "value": "1911" + }, + { + "label": "UltramanPo", + "value": "1976" + }, + { + "label": "unbearable", + "value": "2086" + }, + { + "label": "undeadfish", + "value": "2162" + }, + { + "label": "Upsetting", + "value": "2221" + }, + { + "label": "urbanstar", + "value": "2251" + }, + { + "label": "Undead丨Kn", + "value": "2256" + }, + { + "label": "urbanshark", + "value": "2295" + }, + { + "label": "UnknownTao", + "value": "2459" + }, + { + "label": "undersilve", + "value": "2473" + }, + { + "label": "Undefeated", + "value": "2539" + }, + { + "label": "UrbanMilit", + "value": "2566" + }, + { + "label": "unknown", + "value": "2575" + }, + { + "label": "underlolic", + "value": "2765" + }, + { + "label": "Unintentio", + "value": "2887" + }, + { + "label": "understate", + "value": "2894" + }, + { + "label": "Unlimitedc", + "value": "2907" + }, + { + "label": "Urbanyearn", + "value": "2909" + }, + { + "label": "Unique", + "value": "3059" + }, + { + "label": "Unlucky", + "value": "3068" + }, + { + "label": "Ugly", + "value": "3087" + }, + { + "label": "urbanlove", + "value": "3238" + }, + { + "label": "upgradeflo", + "value": "3251" + }, + { + "label": "urbanhero", + "value": "3273" + }, + { + "label": "Urbanbrain", + "value": "3292" + }, + { + "label": "UrbanRoman", + "value": "3306" + }, + { + "label": "Villain", + "value": "465" + }, + { + "label": "virtualrea", + "value": "45" + }, + { + "label": "Vampires", + "value": "125" + }, + { + "label": "Villainess", + "value": "713" + }, + { + "label": "Videogame", + "value": "3180" + }, + { + "label": "Vampire", + "value": "1693" + }, + { + "label": "VillainPro", + "value": "1676" + }, + { + "label": "VRMMO", + "value": "1004" + }, + { + "label": "Villager", + "value": "1008" + }, + { + "label": "VoiceActor", + "value": "597" + }, + { + "label": "Villainess", + "value": "1655" + }, + { + "label": "VoicePack", + "value": "1371" + }, + { + "label": "VarietySho", + "value": "1684" + }, + { + "label": "videogames", + "value": "3097" + }, + { + "label": "Vlogging", + "value": "878" + }, + { + "label": "VictorianE", + "value": "890" + }, + { + "label": "VersatileM", + "value": "1268" + }, + { + "label": "VillainEvi", + "value": "1375" + }, + { + "label": "vampire", + "value": "1466" + }, + { + "label": "VillIain", + "value": "1538" + }, + { + "label": "Vest", + "value": "1540" + }, + { + "label": "Villains", + "value": "1584" + }, + { + "label": "Viewofthec", + "value": "2015" + }, + { + "label": "vampiredri", + "value": "2564" + }, + { + "label": "VikaBaka", + "value": "2984" + }, + { + "label": "Violence", + "value": "3096" + }, + { + "label": "WebNovel", + "value": "1709" + }, + { + "label": "WeaktoStro", + "value": "273" + }, + { + "label": "WorldHoppi", + "value": "312" + }, + { + "label": "WealthyCha", + "value": "183" + }, + { + "label": "WorldTrave", + "value": "461" + }, + { + "label": "Wars", + "value": "262" + }, + { + "label": "Wizards", + "value": "122" + }, + { + "label": "Withbrutal", + "value": "974" + }, + { + "label": "WeakProtag", + "value": "329" + }, + { + "label": "Writers", + "value": "536" + }, + { + "label": "Wizard", + "value": "867" + }, + { + "label": "Western", + "value": "998" + }, + { + "label": "Witches", + "value": "511" + }, + { + "label": "WorldTree", + "value": "296" + }, + { + "label": "Wuxia", + "value": "730" + }, + { + "label": "World-hopp", + "value": "1226" + }, + { + "label": "Werewolf", + "value": "3144" + }, + { + "label": "WesternFan", + "value": "1436" + }, + { + "label": "Warhammer4", + "value": "141" + }, + { + "label": "Werebeasts", + "value": "689" + }, + { + "label": "War", + "value": "1531" + }, + { + "label": "WorldofWar", + "value": "1746" + }, + { + "label": "Wisdom", + "value": "3206" + }, + { + "label": "Wishes", + "value": "435" + }, + { + "label": "WarRecords", + "value": "1046" + }, + { + "label": "WearBook", + "value": "1199" + }, + { + "label": "WarsWeakto", + "value": "603" + }, + { + "label": "Wilderness", + "value": "852" + }, + { + "label": "Wasteland", + "value": "868" + }, + { + "label": "Wealth", + "value": "1572" + }, + { + "label": "WW2", + "value": "3031" + }, + { + "label": "Witch", + "value": "3040" + }, + { + "label": "Wholesome", + "value": "3058" + }, + { + "label": "wealthyfam", + "value": "3131" + }, + { + "label": "Warlocks", + "value": "121" + }, + { + "label": "Wealthy", + "value": "1113" + }, + { + "label": "weektoStro", + "value": "1121" + }, + { + "label": "Werewolves", + "value": "1192" + }, + { + "label": "WhiteBunSe", + "value": "1308" + }, + { + "label": "WeaktoClan", + "value": "1378" + }, + { + "label": "WorldEmpir", + "value": "1389" + }, + { + "label": "war", + "value": "1455" + }, + { + "label": "WorldWar2", + "value": "1532" + }, + { + "label": "Warship", + "value": "1608" + }, + { + "label": "WealthyCha", + "value": "1609" + }, + { + "label": "WealthChar", + "value": "1638" + }, + { + "label": "Wearabook", + "value": "1648" + }, + { + "label": "wearingabo", + "value": "1761" + }, + { + "label": "wishardtow", + "value": "1860" + }, + { + "label": "writeonlyz", + "value": "1875" + }, + { + "label": "Wuxicheng", + "value": "1880" + }, + { + "label": "WangJiu", + "value": "1885" + }, + { + "label": "WindSpirit", + "value": "1969" + }, + { + "label": "wanderings", + "value": "1997" + }, + { + "label": "What&03", + "value": "2067" + }, + { + "label": "Wanderer", + "value": "2128" + }, + { + "label": "Westernrai", + "value": "2155" + }, + { + "label": "witchfan", + "value": "2192" + }, + { + "label": "watermelon", + "value": "2207" + }, + { + "label": "WasteWoodA", + "value": "2208" + }, + { + "label": "whitekeybo", + "value": "2289" + }, + { + "label": "Winningthe", + "value": "2332" + }, + { + "label": "Walkinthec", + "value": "2338" + }, + { + "label": "Whitehorse", + "value": "2388" + }, + { + "label": "WangXiaomi", + "value": "2396" + }, + { + "label": "witheredpr", + "value": "2460" + }, + { + "label": "Wuhutookof", + "value": "2474" + }, + { + "label": "willowcand", + "value": "2486" + }, + { + "label": "wastefish", + "value": "2487" + }, + { + "label": "WangEr", + "value": "2498" + }, + { + "label": "wanttocome", + "value": "2544" + }, + { + "label": "wanttoeatg", + "value": "2561" + }, + { + "label": "WenGuang", + "value": "2602" + }, + { + "label": "WenXuanyu", + "value": "2610" + }, + { + "label": "WifeistheD", + "value": "2648" + }, + { + "label": "watertown", + "value": "2652" + }, + { + "label": "warmtime", + "value": "2667" + }, + { + "label": "windandmap", + "value": "2676" + }, + { + "label": "Whoringmak", + "value": "2771" + }, + { + "label": "whiteshirt", + "value": "2863" + }, + { + "label": "WOW", + "value": "2934" + }, + { + "label": "WarofCivil", + "value": "2946" + }, + { + "label": "Warlock", + "value": "2994" + }, + { + "label": "wife-chasi", + "value": "3065" + }, + { + "label": "Warcraft", + "value": "3126" + }, + { + "label": "Worlds", + "value": "3151" + }, + { + "label": "Xianxia", + "value": "355" + }, + { + "label": "Xuanhuan", + "value": "743" + }, + { + "label": "XiuXiuXiuX", + "value": "1895" + }, + { + "label": "谢邀", + "value": "1937" + }, + { + "label": "Xiaoxin", + "value": "1983" + }, + { + "label": "Xueqiunder", + "value": "2108" + }, + { + "label": "小封", + "value": "2240" + }, + { + "label": "XuebaIII", + "value": "2274" + }, + { + "label": "Xufamilyel", + "value": "2279" + }, + { + "label": "Xuebaisinv", + "value": "2314" + }, + { + "label": "XuIintheTa", + "value": "2509" + }, + { + "label": "Xiaothreey", + "value": "2535" + }, + { + "label": "XieDaoheng", + "value": "2577" + }, + { + "label": "Xiaonianbl", + "value": "2582" + }, + { + "label": "XiaonianXu", + "value": "2681" + }, + { + "label": "XiaomiStar", + "value": "2710" + }, + { + "label": "XiaoxiangP", + "value": "2899" + }, + { + "label": "Xiuxian", + "value": "3243" + }, + { + "label": "Yandere", + "value": "77" + }, + { + "label": "Yuri", + "value": "152" + }, + { + "label": "YoungerSis", + "value": "359" + }, + { + "label": "Yaoi", + "value": "738" + }, + { + "label": "YoungerLov", + "value": "787" + }, + { + "label": "Youth", + "value": "863" + }, + { + "label": "Yugioh", + "value": "1314" + }, + { + "label": "YeluChengj", + "value": "1869" + }, + { + "label": "YoungerBro", + "value": "816" + }, + { + "label": "Youkai", + "value": "1058" + }, + { + "label": "Yu-Gi-Oh", + "value": "1133" + }, + { + "label": "YellowSpri", + "value": "1827" + }, + { + "label": "youaretoow", + "value": "1930" + }, + { + "label": "YinLiisins", + "value": "1961" + }, + { + "label": "Yongchuang", + "value": "2025" + }, + { + "label": "YingXiaofe", + "value": "2060" + }, + { + "label": "YangXiaoA", + "value": "2106" + }, + { + "label": "YuXiaoqi", + "value": "2176" + }, + { + "label": "Yearningfo", + "value": "2235" + }, + { + "label": "yearningfo", + "value": "2269" + }, + { + "label": "Yunmu", + "value": "2271" + }, + { + "label": "Ying&03", + "value": "2308" + }, + { + "label": "YuboTiandi", + "value": "2318" + }, + { + "label": "Yakult", + "value": "2321" + }, + { + "label": "YeXiaobai", + "value": "2329" + }, + { + "label": "Yearningfo", + "value": "2402" + }, + { + "label": "Yaoyue", + "value": "2536" + }, + { + "label": "YuTsingYi", + "value": "2578" + }, + { + "label": "yearningfo", + "value": "2612" + }, + { + "label": "YeGucheng", + "value": "2711" + }, + { + "label": "YoungMaste", + "value": "2739" + }, + { + "label": "YeGongzi", + "value": "2743" + }, + { + "label": "YuYuyu", + "value": "2754" + }, + { + "label": "yearningfo", + "value": "2805" + }, + { + "label": "YoungMaste", + "value": "2821" + }, + { + "label": "YeQianqiu", + "value": "2825" + }, + { + "label": "yearaftery", + "value": "2885" + }, + { + "label": "YunZhongju", + "value": "2901" + }, + { + "label": "Yearningto", + "value": "2963" + }, + { + "label": "YeYe", + "value": "2972" + }, + { + "label": "Yamen", + "value": "3069" + }, + { + "label": "younglovei", + "value": "3091" + }, + { + "label": "Zombies", + "value": "111" + }, + { + "label": "Zergs", + "value": "1151" + }, + { + "label": "Zombie", + "value": "885" + }, + { + "label": "Zerg", + "value": "1126" + }, + { + "label": "z-man", + "value": "159" + }, + { + "label": "Zoo", + "value": "887" + }, + { + "label": "ZhuZhiyue", + "value": "2163" + }, + { + "label": "ZombieQuee", + "value": "1613" + }, + { + "label": "ZiXuanXuan", + "value": "1940" + }, + { + "label": "Zuge", + "value": "2187" + }, + { + "label": "ZombieGod", + "value": "2193" + }, + { + "label": "ZhugeIrona", + "value": "2204" + }, + { + "label": "zombiefish", + "value": "2296" + }, + { + "label": "Zulongstil", + "value": "2305" + }, + { + "label": "ZhangTianb", + "value": "2379" + }, + { + "label": "ZhuDabald", + "value": "2540" + }, + { + "label": "ZhangJuli", + "value": "2608" + }, + { + "label": "ZhugeDali&", + "value": "2633" + }, + { + "label": "ZhangFeiin", + "value": "2636" + }, + { + "label": "Zhugeiscra", + "value": "2642" + }, + { + "label": "ZhangErgou", + "value": "2714" + }, + { + "label": "ZuwuGonggo", + "value": "2737" + }, + { + "label": "zhishen", + "value": "2760" + }, + { + "label": "Zippo", + "value": "2773" + }, + { + "label": "ZombieSumo", + "value": "2988" + }, + { + "label": "直播", + "value": "3268" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readwn/filters/wuxiamtl.json b/plugins/multisrc/readwn/filters/wuxiamtl.json new file mode 100644 index 000000000..9f8e3351e --- /dev/null +++ b/plugins/multisrc/readwn/filters/wuxiamtl.json @@ -0,0 +1,13160 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Sort By", + "value": "onclick", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "all", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre / Category", + "value": "", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Ecchi", + "value": "ecchi" + }, + { + "label": "Faloo", + "value": "faloo" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Smut", + "value": "smut" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Virtual Reality", + "value": "virtual-reality" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "tags": { + "type": "Picker", + "label": "Tags", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Action", + "value": "118" + }, + { + "label": "AncientChi", + "value": "145" + }, + { + "label": "Academy", + "value": "8" + }, + { + "label": "Adventure", + "value": "933" + }, + { + "label": "Apocalypse", + "value": "201" + }, + { + "label": "ArrogantCh", + "value": "62" + }, + { + "label": "AncientTim", + "value": "146" + }, + { + "label": "Acting", + "value": "52" + }, + { + "label": "Alchemy", + "value": "16" + }, + { + "label": "ArrangedMa", + "value": "163" + }, + { + "label": "ArmyBuildi", + "value": "107" + }, + { + "label": "AlternateW", + "value": "274" + }, + { + "label": "AdaptedtoM", + "value": "164" + }, + { + "label": "Amnesia", + "value": "144" + }, + { + "label": "Aliens", + "value": "40" + }, + { + "label": "AbusiveCha", + "value": "1" + }, + { + "label": "Assassins", + "value": "149" + }, + { + "label": "Aristocrac", + "value": "387" + }, + { + "label": "Artifacts", + "value": "89" + }, + { + "label": "AbsentPare", + "value": "255" + }, + { + "label": "AgeProgres", + "value": "333" + }, + { + "label": "antihero", + "value": "1399" + }, + { + "label": "AntiheroPr", + "value": "323" + }, + { + "label": "AdaptedtoD", + "value": "237" + }, + { + "label": "Army", + "value": "41" + }, + { + "label": "Adventurer", + "value": "165" + }, + { + "label": "AbilitySte", + "value": "97" + }, + { + "label": "Appearance", + "value": "61" + }, + { + "label": "Accelerate", + "value": "59" + }, + { + "label": "AbandonedC", + "value": "395" + }, + { + "label": "AdoptedPro", + "value": "431" + }, + { + "label": "ApatheticP", + "value": "393" + }, + { + "label": "AgeRegress", + "value": "457" + }, + { + "label": "Aggressive", + "value": "416" + }, + { + "label": "AdoptedChi", + "value": "396" + }, + { + "label": "ArtifactCr", + "value": "147" + }, + { + "label": "AdaptedtoD", + "value": "510" + }, + { + "label": "AdaptedtoM", + "value": "88" + }, + { + "label": "Angels", + "value": "353" + }, + { + "label": "AdaptedtoA", + "value": "367" + }, + { + "label": "Adultery", + "value": "292" + }, + { + "label": "Archery", + "value": "215" + }, + { + "label": "Artists", + "value": "494" + }, + { + "label": "Aristocrat", + "value": "724" + }, + { + "label": "ABO", + "value": "780" + }, + { + "label": "AnimalRear", + "value": "493" + }, + { + "label": "Army-build", + "value": "970" + }, + { + "label": "Anal", + "value": "60" + }, + { + "label": "Autism", + "value": "841" + }, + { + "label": "Advancedte", + "value": "3152" + }, + { + "label": "AwkwardPro", + "value": "568" + }, + { + "label": "Anti-HeroL", + "value": "621" + }, + { + "label": "Affair", + "value": "579" + }, + { + "label": "AdaptedtoM", + "value": "471" + }, + { + "label": "AnotherWor", + "value": "921" + }, + { + "label": "Androids", + "value": "598" + }, + { + "label": "AggresiveC", + "value": "809" + }, + { + "label": "alpha", + "value": "2910" + }, + { + "label": "Abandoned", + "value": "3037" + }, + { + "label": "ArtifactsC", + "value": "63" + }, + { + "label": "ArmsDealer", + "value": "398" + }, + { + "label": "Adult", + "value": "1012" + }, + { + "label": "AdaptedtoM", + "value": "1437" + }, + { + "label": "AdaptedtoG", + "value": "106" + }, + { + "label": "ApartmentL", + "value": "994" + }, + { + "label": "Anime", + "value": "1060" + }, + { + "label": "AgeGap", + "value": "1246" + }, + { + "label": "Azeroth", + "value": "1303" + }, + { + "label": "Alternativ", + "value": "2857" + }, + { + "label": "Assassin", + "value": "1161" + }, + { + "label": "AntiqueSho", + "value": "1386" + }, + { + "label": "Angel", + "value": "1190" + }, + { + "label": "Abuse", + "value": "1269" + }, + { + "label": "America", + "value": "2846" + }, + { + "label": "Anti-Magic", + "value": "499" + }, + { + "label": "Ability", + "value": "819" + }, + { + "label": "Astrologer", + "value": "880" + }, + { + "label": "Actors", + "value": "893" + }, + { + "label": "Ancient", + "value": "1142" + }, + { + "label": "AutomaticU", + "value": "1243" + }, + { + "label": "Abilities", + "value": "1279" + }, + { + "label": "AnimalChar", + "value": "334" + }, + { + "label": "AI", + "value": "628" + }, + { + "label": "AlternateH", + "value": "784" + }, + { + "label": "Adrogynous", + "value": "923" + }, + { + "label": "AcceptingD", + "value": "1096" + }, + { + "label": "Actress", + "value": "1118" + }, + { + "label": "Alchemist", + "value": "1278" + }, + { + "label": "animals", + "value": "1282" + }, + { + "label": "AmusementP", + "value": "1408" + }, + { + "label": "Automatons", + "value": "1419" + }, + { + "label": "AbusiveCha", + "value": "1475" + }, + { + "label": "Assasins", + "value": "1525" + }, + { + "label": "Age-gap", + "value": "1794" + }, + { + "label": "AmericanCo", + "value": "2757" + }, + { + "label": "Americas", + "value": "2762" + }, + { + "label": "Abusivelov", + "value": "3009" + }, + { + "label": "Agent", + "value": "3035" + }, + { + "label": "Antagonist", + "value": "3042" + }, + { + "label": "Ashknightw", + "value": "3043" + }, + { + "label": "Actingweak", + "value": "3141" + }, + { + "label": "ArtifactsB", + "value": "148" + }, + { + "label": "Artificial", + "value": "206" + }, + { + "label": "Appearance", + "value": "275" + }, + { + "label": "Anl", + "value": "293" + }, + { + "label": "AncientChi", + "value": "322" + }, + { + "label": "Average-lo", + "value": "378" + }, + { + "label": "Anti-socia", + "value": "532" + }, + { + "label": "AutomaticU", + "value": "655" + }, + { + "label": "AncientBus", + "value": "785" + }, + { + "label": "Adopted", + "value": "844" + }, + { + "label": "Androgynou", + "value": "895" + }, + { + "label": "Appraisal", + "value": "898" + }, + { + "label": "AI-chip", + "value": "902" + }, + { + "label": "Apprentice", + "value": "977" + }, + { + "label": "ArmsTrade", + "value": "1000" + }, + { + "label": "AverageLoo", + "value": "1001" + }, + { + "label": "ancientset", + "value": "1032" + }, + { + "label": "AdaptedtoV", + "value": "1042" + }, + { + "label": "Aggressive", + "value": "1053" + }, + { + "label": "AncientRea", + "value": "1055" + }, + { + "label": "Apocalypti", + "value": "1065" + }, + { + "label": "Arknights", + "value": "1135" + }, + { + "label": "AnotherWor", + "value": "1149" + }, + { + "label": "AdvancedKn", + "value": "1164" + }, + { + "label": "AbandonedC", + "value": "1198" + }, + { + "label": "Aristrocac", + "value": "1201" + }, + { + "label": "ACGN", + "value": "1252" + }, + { + "label": "Abortion", + "value": "1284" + }, + { + "label": "Adoption", + "value": "1285" + }, + { + "label": "AcasualPaw", + "value": "1290" + }, + { + "label": "Animator", + "value": "1305" + }, + { + "label": "AncientWea", + "value": "1338" + }, + { + "label": "artificer", + "value": "1373" + }, + { + "label": "assasin", + "value": "1387" + }, + { + "label": "Artist", + "value": "1468" + }, + { + "label": "Artis", + "value": "1500" + }, + { + "label": "Artifact", + "value": "1506" + }, + { + "label": "Anti-heroP", + "value": "1509" + }, + { + "label": "AlienInvas", + "value": "1526" + }, + { + "label": "ABO(AlphaB", + "value": "1531" + }, + { + "label": "AgeDiffere", + "value": "1580" + }, + { + "label": "ancienttim", + "value": "1586" + }, + { + "label": "AzurLane", + "value": "1594" + }, + { + "label": "Apocalypse", + "value": "1612" + }, + { + "label": "Aftertheso", + "value": "1668" + }, + { + "label": "atravellin", + "value": "1684" + }, + { + "label": "autumnautu", + "value": "1685" + }, + { + "label": "ahveryfish", + "value": "1732" + }, + { + "label": "AfricanEmi", + "value": "1740" + }, + { + "label": "agrass", + "value": "1743" + }, + { + "label": "AgeofGods", + "value": "1748" + }, + { + "label": "Apple", + "value": "1818" + }, + { + "label": "allenzhang", + "value": "1867" + }, + { + "label": "Authoroffa", + "value": "1870" + }, + { + "label": "Aaron&0", + "value": "1912" + }, + { + "label": "Ayanokoji", + "value": "1914" + }, + { + "label": "arayofsuns", + "value": "1979" + }, + { + "label": "animenewco", + "value": "2065" + }, + { + "label": "absolutely", + "value": "2112" + }, + { + "label": "anoldman", + "value": "2126" + }, + { + "label": "Auspicious", + "value": "2146" + }, + { + "label": "askTaichi", + "value": "2181" + }, + { + "label": "angryhouse", + "value": "2196" + }, + { + "label": "AllHeavens", + "value": "2204" + }, + { + "label": "Amagicpill", + "value": "2298" + }, + { + "label": "avigorous", + "value": "2341" + }, + { + "label": "Anautumnra", + "value": "2451" + }, + { + "label": "Archer", + "value": "2474" + }, + { + "label": "Alone", + "value": "2489" + }, + { + "label": "AZanpakut", + "value": "2512" + }, + { + "label": "Aliverday", + "value": "2533" + }, + { + "label": "AlmightyCo", + "value": "2661" + }, + { + "label": "Almightypl", + "value": "2663" + }, + { + "label": "AnlanInvin", + "value": "2721" + }, + { + "label": "AncientChi", + "value": "2751" + }, + { + "label": "AlterateHi", + "value": "2761" + }, + { + "label": "ArmsDealer", + "value": "2766" + }, + { + "label": "Anti-MC", + "value": "2775" + }, + { + "label": "Artificial", + "value": "2777" + }, + { + "label": "Award-winn", + "value": "2778" + }, + { + "label": "adventerer", + "value": "2811" + }, + { + "label": "armoredcit", + "value": "2830" + }, + { + "label": "Abyss", + "value": "2878" + }, + { + "label": "Animation", + "value": "2890" + }, + { + "label": "AnimationD", + "value": "2891" + }, + { + "label": "Avatar", + "value": "2912" + }, + { + "label": "Adventurer", + "value": "2913" + }, + { + "label": "A.I", + "value": "2941" + }, + { + "label": "Actor", + "value": "2990" + }, + { + "label": "ADeadBody", + "value": "3014" + }, + { + "label": "Androgynou", + "value": "3115" + }, + { + "label": "Alljobs", + "value": "3143" + }, + { + "label": "Adventur", + "value": "3163" + }, + { + "label": "Abandoning", + "value": "3174" + }, + { + "label": "Aesthetic", + "value": "3232" + }, + { + "label": "Adaptedfro", + "value": "3247" + }, + { + "label": "BeautifulF", + "value": "50" + }, + { + "label": "BusinessMa", + "value": "66" + }, + { + "label": "BlackBelly", + "value": "151" + }, + { + "label": "Betrayal", + "value": "150" + }, + { + "label": "BeastCompa", + "value": "35" + }, + { + "label": "Businessme", + "value": "238" + }, + { + "label": "Beasts", + "value": "64" + }, + { + "label": "BodyTemper", + "value": "90" + }, + { + "label": "Bloodlines", + "value": "17" + }, + { + "label": "BickeringC", + "value": "53" + }, + { + "label": "Basketball", + "value": "243" + }, + { + "label": "BrokenEnga", + "value": "152" + }, + { + "label": "Bullying", + "value": "549" + }, + { + "label": "BattleComp", + "value": "189" + }, + { + "label": "Beastkin", + "value": "476" + }, + { + "label": "BattleAcad", + "value": "226" + }, + { + "label": "Buddhism", + "value": "259" + }, + { + "label": "BrotherCom", + "value": "302" + }, + { + "label": "Books", + "value": "488" + }, + { + "label": "Brotherhoo", + "value": "369" + }, + { + "label": "Bodyguards", + "value": "261" + }, + { + "label": "Bleach", + "value": "659" + }, + { + "label": "Blackmail", + "value": "294" + }, + { + "label": "BasedonaMo", + "value": "382" + }, + { + "label": "Blacksmith", + "value": "368" + }, + { + "label": "BodySwap", + "value": "54" + }, + { + "label": "beautifulh", + "value": "65" + }, + { + "label": "Business", + "value": "625" + }, + { + "label": "Beasttamin", + "value": "2983" + }, + { + "label": "Biochip", + "value": "388" + }, + { + "label": "BeastTamer", + "value": "660" + }, + { + "label": "Beauty", + "value": "3021" + }, + { + "label": "BasedonaVi", + "value": "3245" + }, + { + "label": "BlindProta", + "value": "575" + }, + { + "label": "Brainwashi", + "value": "421" + }, + { + "label": "Bookworm", + "value": "450" + }, + { + "label": "BasedonaTV", + "value": "593" + }, + { + "label": "BL", + "value": "1501" + }, + { + "label": "Bloodpumpi", + "value": "2985" + }, + { + "label": "BDSM", + "value": "1082" + }, + { + "label": "Bestiality", + "value": "920" + }, + { + "label": "BasedonanA", + "value": "3243" + }, + { + "label": "BloodManip", + "value": "306" + }, + { + "label": "BigBroHasD", + "value": "612" + }, + { + "label": "BisexualPr", + "value": "1326" + }, + { + "label": "Badboy", + "value": "2999" + }, + { + "label": "BlindDates", + "value": "1249" + }, + { + "label": "BeautifulP", + "value": "871" + }, + { + "label": "Bulldozer", + "value": "1218" + }, + { + "label": "Butlers", + "value": "597" + }, + { + "label": "Beast", + "value": "788" + }, + { + "label": "Boxing", + "value": "1446" + }, + { + "label": "Baby", + "value": "3045" + }, + { + "label": "Bully", + "value": "620" + }, + { + "label": "Businessma", + "value": "850" + }, + { + "label": "Billionair", + "value": "1022" + }, + { + "label": "Beautifull", + "value": "1104" + }, + { + "label": "buildingki", + "value": "1388" + }, + { + "label": "BookTransm", + "value": "1575" + }, + { + "label": "blooddemon", + "value": "1951" + }, + { + "label": "Boss", + "value": "2768" + }, + { + "label": "Baseball", + "value": "460" + }, + { + "label": "Blind", + "value": "734" + }, + { + "label": "Bloodline", + "value": "913" + }, + { + "label": "Beastmen", + "value": "968" + }, + { + "label": "BeastCompa", + "value": "1139" + }, + { + "label": "BehindtheS", + "value": "1247" + }, + { + "label": "BungouStra", + "value": "1400" + }, + { + "label": "Businesswo", + "value": "3022" + }, + { + "label": "Boss-Subor", + "value": "34" + }, + { + "label": "bookslikeu", + "value": "489" + }, + { + "label": "BattleThro", + "value": "685" + }, + { + "label": "Babies", + "value": "750" + }, + { + "label": "Black-bell", + "value": "815" + }, + { + "label": "BTTH", + "value": "833" + }, + { + "label": "BookWearer", + "value": "861" + }, + { + "label": "Breakup", + "value": "931" + }, + { + "label": "BunguoStra", + "value": "985" + }, + { + "label": "Bussiness", + "value": "1015" + }, + { + "label": "Beautifulf", + "value": "1021" + }, + { + "label": "BritishEmp", + "value": "1070" + }, + { + "label": "BehindtheS", + "value": "1093" + }, + { + "label": "Bussinesma", + "value": "1102" + }, + { + "label": "BuildKingd", + "value": "1134" + }, + { + "label": "Bloodborne", + "value": "1136" + }, + { + "label": "Blackbelli", + "value": "1156" + }, + { + "label": "Basket", + "value": "1209" + }, + { + "label": "Badassprot", + "value": "1283" + }, + { + "label": "beastman", + "value": "1301" + }, + { + "label": "Biomass", + "value": "1307" + }, + { + "label": "Blacklight", + "value": "1308" + }, + { + "label": "beautifulf", + "value": "1361" + }, + { + "label": "BeautifulF", + "value": "1363" + }, + { + "label": "BusinessEm", + "value": "1469" + }, + { + "label": "BlackTechn", + "value": "1515" + }, + { + "label": "BrortherCo", + "value": "1523" + }, + { + "label": "Band", + "value": "1529" + }, + { + "label": "Banking", + "value": "1538" + }, + { + "label": "Biotechnol", + "value": "1549" + }, + { + "label": "Black-bell", + "value": "1573" + }, + { + "label": "businessor", + "value": "1577" + }, + { + "label": "BusinessDe", + "value": "1587" + }, + { + "label": "BlackBelly", + "value": "1590" + }, + { + "label": "bigpicture", + "value": "1648" + }, + { + "label": "BusinessRi", + "value": "1664" + }, + { + "label": "BloodofAni", + "value": "1671" + }, + { + "label": "Biscuits", + "value": "1690" + }, + { + "label": "Brownsugar", + "value": "1717" + }, + { + "label": "blackandwh", + "value": "1722" + }, + { + "label": "Bigplayers", + "value": "1733" + }, + { + "label": "bigwhitewh", + "value": "1762" + }, + { + "label": "Becomefamo", + "value": "1773" + }, + { + "label": "bloomingon", + "value": "1802" + }, + { + "label": "ButterflyD", + "value": "1830" + }, + { + "label": "Boundlessf", + "value": "1841" + }, + { + "label": "belovedbab", + "value": "1843" + }, + { + "label": "breezesilv", + "value": "1892" + }, + { + "label": "Bigdog", + "value": "1895" + }, + { + "label": "BuLofan", + "value": "1916" + }, + { + "label": "Brightmoon", + "value": "1926" + }, + { + "label": "Breeze", + "value": "1958" + }, + { + "label": "BraisedPai", + "value": "1991" + }, + { + "label": "BingtangHu", + "value": "1992" + }, + { + "label": "BookstoreS", + "value": "1994" + }, + { + "label": "blacksoil", + "value": "1998" + }, + { + "label": "Bearcat", + "value": "1999" + }, + { + "label": "BrotherChe", + "value": "2035" + }, + { + "label": "blueshirts", + "value": "2047" + }, + { + "label": "beatyourse", + "value": "2061" + }, + { + "label": "bluestone", + "value": "2063" + }, + { + "label": "bitefire", + "value": "2070" + }, + { + "label": "blackandim", + "value": "2077" + }, + { + "label": "Bearchildl", + "value": "2085" + }, + { + "label": "BloodMoonG", + "value": "2089" + }, + { + "label": "BigSkeleto", + "value": "2091" + }, + { + "label": "BrotherZhu", + "value": "2092" + }, + { + "label": "BarrenEmpe", + "value": "2117" + }, + { + "label": "breaktheke", + "value": "2123" + }, + { + "label": "beastprota", + "value": "2165" + }, + { + "label": "bigorangew", + "value": "2168" + }, + { + "label": "baldnessat", + "value": "2173" + }, + { + "label": "bigcitysma", + "value": "2220" + }, + { + "label": "baldman", + "value": "2248" + }, + { + "label": "BoXiaowen", + "value": "2252" + }, + { + "label": "Belltouche", + "value": "2257" + }, + { + "label": "BookDustSp", + "value": "2267" + }, + { + "label": "broalwaysg", + "value": "2353" + }, + { + "label": "Bodhicitta", + "value": "2382" + }, + { + "label": "beaming", + "value": "2384" + }, + { + "label": "Breakingth", + "value": "2441" + }, + { + "label": "billionpeo", + "value": "2444" + }, + { + "label": "Buildthewo", + "value": "2452" + }, + { + "label": "Bigcockcut", + "value": "2471" + }, + { + "label": "bigtent", + "value": "2483" + }, + { + "label": "boycold", + "value": "2499" + }, + { + "label": "Bringaknif", + "value": "2515" + }, + { + "label": "becausesoh", + "value": "2524" + }, + { + "label": "bearcocoa", + "value": "2529" + }, + { + "label": "bluesilksu", + "value": "2559" + }, + { + "label": "bighippo", + "value": "2567" + }, + { + "label": "beautifula", + "value": "2568" + }, + { + "label": "burnout", + "value": "2574" + }, + { + "label": "Burningmou", + "value": "2581" + }, + { + "label": "beggingfor", + "value": "2584" + }, + { + "label": "blackcatis", + "value": "2589" + }, + { + "label": "BlackDrago", + "value": "2601" + }, + { + "label": "Beansandgr", + "value": "2667" + }, + { + "label": "Boiled", + "value": "2674" + }, + { + "label": "blackandwh", + "value": "2677" + }, + { + "label": "BaiXiaowei", + "value": "2698" + }, + { + "label": "bewitching", + "value": "2709" + }, + { + "label": "balduncle", + "value": "2710" + }, + { + "label": "bluesilk", + "value": "2712" + }, + { + "label": "Bandit", + "value": "2796" + }, + { + "label": "BuddhaofNi", + "value": "2807" + }, + { + "label": "blackice", + "value": "2808" + }, + { + "label": "BearChild", + "value": "2821" + }, + { + "label": "BoyxBoy", + "value": "2885" + }, + { + "label": "BrotherInL", + "value": "2920" + }, + { + "label": "Blackening", + "value": "2926" + }, + { + "label": "bickeringl", + "value": "2954" + }, + { + "label": "Beatthemal", + "value": "2993" + }, + { + "label": "Beatthefem", + "value": "2994" + }, + { + "label": "Beastamer", + "value": "3052" + }, + { + "label": "Beautifulc", + "value": "3066" + }, + { + "label": "Beauties", + "value": "3119" + }, + { + "label": "BecomeLove", + "value": "3129" + }, + { + "label": "backstabbi", + "value": "3139" + }, + { + "label": "Betrayed", + "value": "3147" + }, + { + "label": "Bigshot", + "value": "3151" + }, + { + "label": "BeautifulH", + "value": "3153" + }, + { + "label": "building", + "value": "3216" + }, + { + "label": "Bgfellow", + "value": "3231" + }, + { + "label": "BasedonaVi", + "value": "3246" + }, + { + "label": "Cultivatio", + "value": "20" + }, + { + "label": "CalmProtag", + "value": "18" + }, + { + "label": "CleverProt", + "value": "9" + }, + { + "label": "Cheats", + "value": "36" + }, + { + "label": "Celebritie", + "value": "55" + }, + { + "label": "CunningPro", + "value": "195" + }, + { + "label": "ColdLoveIn", + "value": "56" + }, + { + "label": "ComedicUnd", + "value": "91" + }, + { + "label": "comedy", + "value": "829" + }, + { + "label": "Childcare", + "value": "44" + }, + { + "label": "ColdProtag", + "value": "37" + }, + { + "label": "CharacterG", + "value": "67" + }, + { + "label": "CuteProtag", + "value": "452" + }, + { + "label": "Cooking", + "value": "315" + }, + { + "label": "CaringProt", + "value": "435" + }, + { + "label": "CautiousPr", + "value": "108" + }, + { + "label": "CuteChildr", + "value": "239" + }, + { + "label": "ConfidentP", + "value": "346" + }, + { + "label": "CharmingPr", + "value": "347" + }, + { + "label": "CoupleGrow", + "value": "526" + }, + { + "label": "CollegeUni", + "value": "192" + }, + { + "label": "CarefreePr", + "value": "295" + }, + { + "label": "CruelChara", + "value": "68" + }, + { + "label": "CuteStory", + "value": "380" + }, + { + "label": "Cross-dres", + "value": "210" + }, + { + "label": "ChildhoodF", + "value": "370" + }, + { + "label": "ChildProta", + "value": "268" + }, + { + "label": "ClingyLove", + "value": "277" + }, + { + "label": "Cheat", + "value": "705" + }, + { + "label": "ChildhoodL", + "value": "430" + }, + { + "label": "CardGames", + "value": "782" + }, + { + "label": "Crime", + "value": "556" + }, + { + "label": "ChatRooms", + "value": "267" + }, + { + "label": "ChinesePre", + "value": "639" + }, + { + "label": "CosmicWars", + "value": "219" + }, + { + "label": "Crossdress", + "value": "756" + }, + { + "label": "Crafting", + "value": "327" + }, + { + "label": "ChildAbuse", + "value": "470" + }, + { + "label": "Ceo", + "value": "2976" + }, + { + "label": "ClanBuildi", + "value": "19" + }, + { + "label": "Contracts", + "value": "379" + }, + { + "label": "ClanSectDe", + "value": "662" + }, + { + "label": "Conquer", + "value": "2987" + }, + { + "label": "Chefs", + "value": "314" + }, + { + "label": "ClumsyLove", + "value": "473" + }, + { + "label": "Clones", + "value": "448" + }, + { + "label": "Cohabitati", + "value": "474" + }, + { + "label": "Campus", + "value": "1271" + }, + { + "label": "CharacterD", + "value": "119" + }, + { + "label": "ChildhoodS", + "value": "376" + }, + { + "label": "CowardlyPr", + "value": "100" + }, + { + "label": "ChatGroup", + "value": "673" + }, + { + "label": "ChildishPr", + "value": "418" + }, + { + "label": "Cannibalis", + "value": "472" + }, + { + "label": "Criminals", + "value": "290" + }, + { + "label": "CourtOffic", + "value": "485" + }, + { + "label": "Crossover", + "value": "269" + }, + { + "label": "Corruption", + "value": "550" + }, + { + "label": "Curses", + "value": "516" + }, + { + "label": "CampusLove", + "value": "375" + }, + { + "label": "Counteratt", + "value": "1225" + }, + { + "label": "ChildhoodP", + "value": "475" + }, + { + "label": "ComingofAg", + "value": "1147" + }, + { + "label": "Cousins", + "value": "519" + }, + { + "label": "ChoiceSele", + "value": "647" + }, + { + "label": "Confinemen", + "value": "912" + }, + { + "label": "Crossing", + "value": "2760" + }, + { + "label": "ciweimao", + "value": "3233" + }, + { + "label": "Conditiona", + "value": "413" + }, + { + "label": "Co-Workers", + "value": "454" + }, + { + "label": "Coma", + "value": "779" + }, + { + "label": "Celebrity", + "value": "979" + }, + { + "label": "CuriousPro", + "value": "1275" + }, + { + "label": "Cute", + "value": "1258" + }, + { + "label": "Chuunibyou", + "value": "522" + }, + { + "label": "CoolText", + "value": "736" + }, + { + "label": "Creation", + "value": "837" + }, + { + "label": "College", + "value": "1018" + }, + { + "label": "Clubs", + "value": "1126" + }, + { + "label": "Cityurban", + "value": "3225" + }, + { + "label": "Childbirth", + "value": "949" + }, + { + "label": "Chronology", + "value": "1014" + }, + { + "label": "Cthulhu", + "value": "1137" + }, + { + "label": "Creatures", + "value": "307" + }, + { + "label": "Cards", + "value": "350" + }, + { + "label": "CosmicHorr", + "value": "757" + }, + { + "label": "CrazyProta", + "value": "758" + }, + { + "label": "Conflictin", + "value": "796" + }, + { + "label": "CampusLife", + "value": "816" + }, + { + "label": "Contract", + "value": "891" + }, + { + "label": "CautiousMc", + "value": "904" + }, + { + "label": "Cultivator", + "value": "958" + }, + { + "label": "Card", + "value": "981" + }, + { + "label": "CuteChild", + "value": "1016" + }, + { + "label": "Capitalism", + "value": "1071" + }, + { + "label": "Creator", + "value": "1148" + }, + { + "label": "Civilizati", + "value": "1215" + }, + { + "label": "CommonerLi", + "value": "1317" + }, + { + "label": "ColdLoveIn", + "value": "1365" + }, + { + "label": "cunningmc", + "value": "1380" + }, + { + "label": "Cryostasis", + "value": "1447" + }, + { + "label": "Cosplay", + "value": "1460" + }, + { + "label": "Civilizati", + "value": "1550" + }, + { + "label": "ComedicUnd", + "value": "1568" + }, + { + "label": "CelestialC", + "value": "1986" + }, + { + "label": "ColdNightL", + "value": "2695" + }, + { + "label": "cunning", + "value": "2755" + }, + { + "label": "City", + "value": "3083" + }, + { + "label": "ComplexFam", + "value": "153" + }, + { + "label": "Charismati", + "value": "363" + }, + { + "label": "CleverProt", + "value": "523" + }, + { + "label": "Cluelessly", + "value": "667" + }, + { + "label": "cunningfem", + "value": "795" + }, + { + "label": "chat-room", + "value": "798" + }, + { + "label": "Companies", + "value": "824" + }, + { + "label": "Complicate", + "value": "836" + }, + { + "label": "ChildhoodS", + "value": "863" + }, + { + "label": "Collection", + "value": "873" + }, + { + "label": "Colonializ", + "value": "915" + }, + { + "label": "ChinaRefor", + "value": "950" + }, + { + "label": "Church", + "value": "954" + }, + { + "label": "Chaos", + "value": "957" + }, + { + "label": "CluelessPr", + "value": "963" + }, + { + "label": "ChuningMC", + "value": "1002" + }, + { + "label": "CivilServa", + "value": "1072" + }, + { + "label": "Conspirati", + "value": "1103" + }, + { + "label": "CuteProtag", + "value": "1105" + }, + { + "label": "CuteMaleLe", + "value": "1113" + }, + { + "label": "CaringMale", + "value": "1152" + }, + { + "label": "Comic", + "value": "1167" + }, + { + "label": "CunningPro", + "value": "1168" + }, + { + "label": "Club", + "value": "1184" + }, + { + "label": "Competitio", + "value": "1210" + }, + { + "label": "ChildhoodE", + "value": "1229" + }, + { + "label": "cluthullu", + "value": "1264" + }, + { + "label": "ChineseAnc", + "value": "1280" + }, + { + "label": "Celestials", + "value": "1294" + }, + { + "label": "Curse", + "value": "1299" + }, + { + "label": "ChinaNamba", + "value": "1306" + }, + { + "label": "ChenHegao", + "value": "1309" + }, + { + "label": "Contagonis", + "value": "1310" + }, + { + "label": "CutePet", + "value": "1339" + }, + { + "label": "Chinese", + "value": "1342" + }, + { + "label": "chat", + "value": "1377" + }, + { + "label": "codegeass", + "value": "1398" + }, + { + "label": "Cunnilingu", + "value": "1455" + }, + { + "label": "Commandand", + "value": "1472" + }, + { + "label": "Criminolog", + "value": "1476" + }, + { + "label": "Chef", + "value": "1480" + }, + { + "label": "CalmMalePr", + "value": "1488" + }, + { + "label": "Colonizati", + "value": "1494" + }, + { + "label": "Capitalist", + "value": "1516" + }, + { + "label": "Crimes", + "value": "1527" + }, + { + "label": "Casinos", + "value": "1539" + }, + { + "label": "ColonialEr", + "value": "1556" + }, + { + "label": "Colony", + "value": "1557" + }, + { + "label": "CubRaising", + "value": "1565" + }, + { + "label": "ColdMaleLe", + "value": "1567" + }, + { + "label": "ContractLo", + "value": "1581" + }, + { + "label": "ComplexFam", + "value": "1583" + }, + { + "label": "CareerOrie", + "value": "1595" + }, + { + "label": "Constructi", + "value": "1624" + }, + { + "label": "coffeewith", + "value": "1656" + }, + { + "label": "coverthesu", + "value": "1688" + }, + { + "label": "Chirika", + "value": "1696" + }, + { + "label": "ColdStar&a", + "value": "1724" + }, + { + "label": "chaoticwor", + "value": "1730" + }, + { + "label": "catdaylist", + "value": "1756" + }, + { + "label": "CucumberHa", + "value": "1765" + }, + { + "label": "cloudysky", + "value": "1776" + }, + { + "label": "ChocolateI", + "value": "1777" + }, + { + "label": "CloudTop丨", + "value": "1799" + }, + { + "label": "CherryBlos", + "value": "1811" + }, + { + "label": "ChiDongdon", + "value": "1813" + }, + { + "label": "cuteshadow", + "value": "1816" + }, + { + "label": "Canolaflow", + "value": "1828" + }, + { + "label": "coyote", + "value": "1837" + }, + { + "label": "ChanelNo.1", + "value": "1849" + }, + { + "label": "CloudSummi", + "value": "1850" + }, + { + "label": "camera", + "value": "1856" + }, + { + "label": "canfly", + "value": "1864" + }, + { + "label": "catthatwan", + "value": "1876" + }, + { + "label": "coffeefatc", + "value": "1900" + }, + { + "label": "Cloudseest", + "value": "1904" + }, + { + "label": "chef&03", + "value": "1943" + }, + { + "label": "Cloudtopfi", + "value": "1944" + }, + { + "label": "coldrivers", + "value": "1956" + }, + { + "label": "Comeon", + "value": "1961" + }, + { + "label": "ChenTwelve", + "value": "1980" + }, + { + "label": "caviar", + "value": "1982" + }, + { + "label": "CloudTop丨", + "value": "1989" + }, + { + "label": "Catchtheca", + "value": "1996" + }, + { + "label": "cutegrapef", + "value": "2004" + }, + { + "label": "cartoonwil", + "value": "2023" + }, + { + "label": "ChefSurviv", + "value": "2031" + }, + { + "label": "Cloudtop丨", + "value": "2062" + }, + { + "label": "Canteendry", + "value": "2082" + }, + { + "label": "Crazyforam", + "value": "2083" + }, + { + "label": "Changeever", + "value": "2087" + }, + { + "label": "Comprehens", + "value": "2093" + }, + { + "label": "Catswithfi", + "value": "2130" + }, + { + "label": "CityGod", + "value": "2138" + }, + { + "label": "Cancat", + "value": "2147" + }, + { + "label": "catthousan", + "value": "2183" + }, + { + "label": "Cicadasand", + "value": "2195" + }, + { + "label": "ChenChangf", + "value": "2199" + }, + { + "label": "championge", + "value": "2228" + }, + { + "label": "Crazystory", + "value": "2260" + }, + { + "label": "Can&039", + "value": "2268" + }, + { + "label": "callthebea", + "value": "2278" + }, + { + "label": "catgod", + "value": "2318" + }, + { + "label": "coldcolddo", + "value": "2326" + }, + { + "label": "CokeII", + "value": "2329" + }, + { + "label": "coffeeinst", + "value": "2400" + }, + { + "label": "Chosen12", + "value": "2410" + }, + { + "label": "catloveson", + "value": "2415" + }, + { + "label": "civetcatat", + "value": "2437" + }, + { + "label": "catisrisin", + "value": "2446" + }, + { + "label": "catpowerfi", + "value": "2505" + }, + { + "label": "Can&039", + "value": "2506" + }, + { + "label": "Caicolorsh", + "value": "2516" + }, + { + "label": "CorpseFrag", + "value": "2543" + }, + { + "label": "codewordge", + "value": "2572" + }, + { + "label": "CarambolaJ", + "value": "2582" + }, + { + "label": "cockroache", + "value": "2604" + }, + { + "label": "city​​ya", + "value": "2607" + }, + { + "label": "Codeuntilt", + "value": "2626" + }, + { + "label": "cutepomelo", + "value": "2646" + }, + { + "label": "cloudmadeo", + "value": "2654" + }, + { + "label": "chasingthe", + "value": "2656" + }, + { + "label": "crookeddoo", + "value": "2659" + }, + { + "label": "Cantaloupe", + "value": "2662" + }, + { + "label": "cateatingp", + "value": "2669" + }, + { + "label": "Cupola", + "value": "2683" + }, + { + "label": "cornjuice", + "value": "2706" + }, + { + "label": "cutelovein", + "value": "2733" + }, + { + "label": "CampusRoma", + "value": "2738" + }, + { + "label": "ChainsawMa", + "value": "2747" + }, + { + "label": "Cruel", + "value": "2779" + }, + { + "label": "CangxueFei", + "value": "2786" + }, + { + "label": "Childhoodf", + "value": "2805" + }, + { + "label": "Cultivatio", + "value": "2812" + }, + { + "label": "Conspiracy", + "value": "2816" + }, + { + "label": "Calm", + "value": "2822" + }, + { + "label": "crimesolvi", + "value": "2841" + }, + { + "label": "curechildr", + "value": "2848" + }, + { + "label": "Cultivatio", + "value": "2849" + }, + { + "label": "child", + "value": "2883" + }, + { + "label": "Chinesenov", + "value": "2886" + }, + { + "label": "CountrySid", + "value": "2917" + }, + { + "label": "Calmdown", + "value": "2931" + }, + { + "label": "CatchaGhos", + "value": "3016" + }, + { + "label": "ColdMistre", + "value": "3036" + }, + { + "label": "Crush", + "value": "3047" + }, + { + "label": "Contractma", + "value": "3072" + }, + { + "label": "Culinary", + "value": "3076" + }, + { + "label": "Cultivatio", + "value": "3093" + }, + { + "label": "Cuteadorab", + "value": "3116" + }, + { + "label": "Cultivatio", + "value": "3117" + }, + { + "label": "Castlecult", + "value": "3125" + }, + { + "label": "Champions", + "value": "3130" + }, + { + "label": "contempora", + "value": "3138" + }, + { + "label": "Complete", + "value": "3140" + }, + { + "label": "Cultivatin", + "value": "3145" + }, + { + "label": "Comed", + "value": "3167" + }, + { + "label": "Coolguy", + "value": "3190" + }, + { + "label": "ClassroomO", + "value": "3213" + }, + { + "label": "carpenter", + "value": "3221" + }, + { + "label": "Demons", + "value": "209" + }, + { + "label": "DevotedLov", + "value": "155" + }, + { + "label": "DotingLove", + "value": "156" + }, + { + "label": "Dragons", + "value": "233" + }, + { + "label": "Doctors", + "value": "169" + }, + { + "label": "DouluoDalu", + "value": "85" + }, + { + "label": "Dark", + "value": "328" + }, + { + "label": "DenseProta", + "value": "10" + }, + { + "label": "DotingPare", + "value": "316" + }, + { + "label": "Depictions", + "value": "329" + }, + { + "label": "DotingOlde", + "value": "432" + }, + { + "label": "DeathofLov", + "value": "38" + }, + { + "label": "Drama", + "value": "723" + }, + { + "label": "Daoism", + "value": "93" + }, + { + "label": "Detectives", + "value": "557" + }, + { + "label": "DemonLord", + "value": "154" + }, + { + "label": "Discrimina", + "value": "286" + }, + { + "label": "Death", + "value": "542" + }, + { + "label": "Disabiliti", + "value": "603" + }, + { + "label": "Divorce", + "value": "257" + }, + { + "label": "DaoCompreh", + "value": "92" + }, + { + "label": "Dragon", + "value": "939" + }, + { + "label": "Dwarfs", + "value": "132" + }, + { + "label": "Dungeons", + "value": "469" + }, + { + "label": "Demi-Human", + "value": "571" + }, + { + "label": "DetectiveC", + "value": "927" + }, + { + "label": "DragonBall", + "value": "648" + }, + { + "label": "DomesticAf", + "value": "429" + }, + { + "label": "Destiny", + "value": "120" + }, + { + "label": "DaoCompani", + "value": "521" + }, + { + "label": "Devil", + "value": "3078" + }, + { + "label": "DollsPuppe", + "value": "196" + }, + { + "label": "Dwarves", + "value": "610" + }, + { + "label": "DiscipleTr", + "value": "664" + }, + { + "label": "DarkFantas", + "value": "759" + }, + { + "label": "Dreams", + "value": "284" + }, + { + "label": "Depression", + "value": "906" + }, + { + "label": "Drugs", + "value": "934" + }, + { + "label": "DC", + "value": "669" + }, + { + "label": "Doctor", + "value": "749" + }, + { + "label": "Divination", + "value": "308" + }, + { + "label": "Demon", + "value": "748" + }, + { + "label": "Doomsday", + "value": "877" + }, + { + "label": "Detective", + "value": "774" + }, + { + "label": "DeadProtag", + "value": "541" + }, + { + "label": "DemonSlaye", + "value": "695" + }, + { + "label": "DragonSlay", + "value": "586" + }, + { + "label": "Delinquent", + "value": "605" + }, + { + "label": "Debts", + "value": "524" + }, + { + "label": "Danmei", + "value": "580" + }, + { + "label": "Druids", + "value": "842" + }, + { + "label": "Disfigurem", + "value": "1110" + }, + { + "label": "Dramatic", + "value": "3069" + }, + { + "label": "DungeonMas", + "value": "600" + }, + { + "label": "DragonRide", + "value": "793" + }, + { + "label": "Director", + "value": "760" + }, + { + "label": "dotinglove", + "value": "974" + }, + { + "label": "DishonestP", + "value": "1405" + }, + { + "label": "Dystopia", + "value": "1439" + }, + { + "label": "Dancers", + "value": "1445" + }, + { + "label": "Dream", + "value": "859" + }, + { + "label": "Dinosaurs", + "value": "878" + }, + { + "label": "DeepLTrans", + "value": "969" + }, + { + "label": "DotingPare", + "value": "1265" + }, + { + "label": "Dynasty", + "value": "2817" + }, + { + "label": "DarkDeatho", + "value": "561" + }, + { + "label": "Devils", + "value": "638" + }, + { + "label": "DoupoBTTH", + "value": "719" + }, + { + "label": "Doupo", + "value": "834" + }, + { + "label": "dungeon", + "value": "944" + }, + { + "label": "Digimon", + "value": "1392" + }, + { + "label": "Delusions", + "value": "1422" + }, + { + "label": "DevotedLov", + "value": "1514" + }, + { + "label": "Douluo", + "value": "1553" + }, + { + "label": "Demondomai", + "value": "2266" + }, + { + "label": "dreamblizz", + "value": "2691" + }, + { + "label": "Determined", + "value": "227" + }, + { + "label": "DemonicCul", + "value": "232" + }, + { + "label": "DifferentS", + "value": "381" + }, + { + "label": "Depictions", + "value": "729" + }, + { + "label": "DoubleRebi", + "value": "737" + }, + { + "label": "Doujin", + "value": "820" + }, + { + "label": "DoubleLife", + "value": "905" + }, + { + "label": "DarkPower", + "value": "961" + }, + { + "label": "differentw", + "value": "965" + }, + { + "label": "dotingfami", + "value": "973" + }, + { + "label": "DiscipleLo", + "value": "978" + }, + { + "label": "Diplomats", + "value": "1073" + }, + { + "label": "Dominator", + "value": "1090" + }, + { + "label": "DestinedLo", + "value": "1124" + }, + { + "label": "Doomdays", + "value": "1125" + }, + { + "label": "Dwarf", + "value": "1159" + }, + { + "label": "Disobedien", + "value": "1237" + }, + { + "label": "DotingSibl", + "value": "1266" + }, + { + "label": "DisabledPr", + "value": "1281" + }, + { + "label": "DoingBusin", + "value": "1318" + }, + { + "label": "Devotedlov", + "value": "1346" + }, + { + "label": "Dog", + "value": "1364" + }, + { + "label": "Distrustfu", + "value": "1438" + }, + { + "label": "Divination", + "value": "1466" + }, + { + "label": "DivineProt", + "value": "1487" + }, + { + "label": "Directors", + "value": "1554" + }, + { + "label": "DevilPosse", + "value": "1555" + }, + { + "label": "DumbProtag", + "value": "1603" + }, + { + "label": "DemonsFami", + "value": "1617" + }, + { + "label": "DevotedCou", + "value": "1628" + }, + { + "label": "dreamleave", + "value": "1646" + }, + { + "label": "divinesign", + "value": "1653" + }, + { + "label": "darkpirate", + "value": "1666" + }, + { + "label": "darknight", + "value": "1731" + }, + { + "label": "dragracing", + "value": "1784" + }, + { + "label": "DatangDaqi", + "value": "1787" + }, + { + "label": "dancetofig", + "value": "1803" + }, + { + "label": "Decadeligh", + "value": "1812" + }, + { + "label": "deepbluese", + "value": "1821" + }, + { + "label": "don&039", + "value": "1823" + }, + { + "label": "DatangErwu", + "value": "1836" + }, + { + "label": "Datangsupe", + "value": "1854" + }, + { + "label": "DragonPala", + "value": "1857" + }, + { + "label": "digitalold", + "value": "1872" + }, + { + "label": "DouTuKing", + "value": "1908" + }, + { + "label": "don&039", + "value": "1911" + }, + { + "label": "daughterco", + "value": "1934" + }, + { + "label": "Dreamofthe", + "value": "2012" + }, + { + "label": "DamingYong", + "value": "2019" + }, + { + "label": "DaoyanShen", + "value": "2043" + }, + { + "label": "DemonInvas", + "value": "2081" + }, + { + "label": "DaqingXiao", + "value": "2095" + }, + { + "label": "Dollsister", + "value": "2110" + }, + { + "label": "Devilveget", + "value": "2148" + }, + { + "label": "DragonBall", + "value": "2150" + }, + { + "label": "doyoueator", + "value": "2153" + }, + { + "label": "Destroyerf", + "value": "2170" + }, + { + "label": "deadfatfas", + "value": "2179" + }, + { + "label": "Dahunjun", + "value": "2201" + }, + { + "label": "Desperatel", + "value": "2209" + }, + { + "label": "DatangDaqi", + "value": "2225" + }, + { + "label": "dragon-eat", + "value": "2254" + }, + { + "label": "dreamintot", + "value": "2272" + }, + { + "label": "Dashuaihen", + "value": "2279" + }, + { + "label": "Daddywants", + "value": "2291" + }, + { + "label": "dogeggsold", + "value": "2321" + }, + { + "label": "dreamcatch", + "value": "2348" + }, + { + "label": "DivineBook", + "value": "2350" + }, + { + "label": "Don&039", + "value": "2361" + }, + { + "label": "doyouwantc", + "value": "2364" + }, + { + "label": "dagougou", + "value": "2387" + }, + { + "label": "DriftwoodD", + "value": "2390" + }, + { + "label": "Daybyday", + "value": "2418" + }, + { + "label": "Dikabenka", + "value": "2422" + }, + { + "label": "Diga", + "value": "2430" + }, + { + "label": "Donotbecon", + "value": "2440" + }, + { + "label": "Donotforge", + "value": "2445" + }, + { + "label": "digthreefe", + "value": "2472" + }, + { + "label": "Doomsdaywa", + "value": "2519" + }, + { + "label": "DoctorData", + "value": "2538" + }, + { + "label": "DragonandL", + "value": "2554" + }, + { + "label": "DemonKing", + "value": "2565" + }, + { + "label": "dirtylittl", + "value": "2610" + }, + { + "label": "Drunklifed", + "value": "2720" + }, + { + "label": "Datangpota", + "value": "2728" + }, + { + "label": "dimensiona", + "value": "2737" + }, + { + "label": "Doraemon", + "value": "2748" + }, + { + "label": "Domineerin", + "value": "2769" + }, + { + "label": "Decisive", + "value": "2794" + }, + { + "label": "DemonPower", + "value": "2832" + }, + { + "label": "DragonPowe", + "value": "2833" + }, + { + "label": "DecisiveMc", + "value": "2905" + }, + { + "label": "disability", + "value": "2944" + }, + { + "label": "Diplomacy", + "value": "2967" + }, + { + "label": "Dystopian", + "value": "3002" + }, + { + "label": "Detailed", + "value": "3024" + }, + { + "label": "Desperate", + "value": "3061" + }, + { + "label": "Donaldtrum", + "value": "3084" + }, + { + "label": "DarkForest", + "value": "3090" + }, + { + "label": "Deityhero", + "value": "3118" + }, + { + "label": "Divineacti", + "value": "3121" + }, + { + "label": "Dragoncult", + "value": "3126" + }, + { + "label": "Diggingtos", + "value": "3135" + }, + { + "label": "Devouringt", + "value": "3142" + }, + { + "label": "Divinechoo", + "value": "3155" + }, + { + "label": "Dotingmale", + "value": "3171" + }, + { + "label": "Devourande", + "value": "3180" + }, + { + "label": "DualCultiv", + "value": "3208" + }, + { + "label": "Evolution", + "value": "202" + }, + { + "label": "EarlyRoman", + "value": "2" + }, + { + "label": "Elves", + "value": "133" + }, + { + "label": "Entertainm", + "value": "187" + }, + { + "label": "EvilGods", + "value": "134" + }, + { + "label": "EvilProtag", + "value": "336" + }, + { + "label": "EnemiesBec", + "value": "324" + }, + { + "label": "Episodic", + "value": "438" + }, + { + "label": "e-Sports", + "value": "513" + }, + { + "label": "ElementalM", + "value": "309" + }, + { + "label": "EyePowers", + "value": "170" + }, + { + "label": "Entertainm", + "value": "738" + }, + { + "label": "EuropeanAm", + "value": "851" + }, + { + "label": "EideticMem", + "value": "234" + }, + { + "label": "Empires", + "value": "517" + }, + { + "label": "EasyGoingL", + "value": "287" + }, + { + "label": "EarthInvas", + "value": "554" + }, + { + "label": "Economics", + "value": "288" + }, + { + "label": "EvilOrgani", + "value": "69" + }, + { + "label": "Exorcism", + "value": "481" + }, + { + "label": "Eunuch", + "value": "121" + }, + { + "label": "Engagement", + "value": "158" + }, + { + "label": "EnemiesBec", + "value": "198" + }, + { + "label": "EvilReligi", + "value": "495" + }, + { + "label": "Ecchi", + "value": "2974" + }, + { + "label": "enemiestol", + "value": "1397" + }, + { + "label": "easternfan", + "value": "534" + }, + { + "label": "Egoist", + "value": "3075" + }, + { + "label": "Engineer", + "value": "581" + }, + { + "label": "Experience", + "value": "686" + }, + { + "label": "Exhaustion", + "value": "2868" + }, + { + "label": "Enlightenm", + "value": "606" + }, + { + "label": "Elf", + "value": "838" + }, + { + "label": "Europe", + "value": "2916" + }, + { + "label": "EvilGod", + "value": "955" + }, + { + "label": "Evilmc", + "value": "3186" + }, + { + "label": "EconomicsE", + "value": "157" + }, + { + "label": "Editors", + "value": "946" + }, + { + "label": "EuropeanAm", + "value": "1052" + }, + { + "label": "EnemytoLov", + "value": "1230" + }, + { + "label": "Evergrande", + "value": "2592" + }, + { + "label": "Exploratio", + "value": "3131" + }, + { + "label": "Emotionall", + "value": "338" + }, + { + "label": "eincarnate", + "value": "616" + }, + { + "label": "Entertainm", + "value": "752" + }, + { + "label": "EvilCharac", + "value": "761" + }, + { + "label": "Elite", + "value": "817" + }, + { + "label": "EnemytoLov", + "value": "864" + }, + { + "label": "Entertaime", + "value": "892" + }, + { + "label": "Exorcist", + "value": "908" + }, + { + "label": "Empire", + "value": "916" + }, + { + "label": "eyepower", + "value": "987" + }, + { + "label": "EvilOrgani", + "value": "991" + }, + { + "label": "Evil-prota", + "value": "1056" + }, + { + "label": "Emperialpo", + "value": "1101" + }, + { + "label": "Ex-girlfri", + "value": "1128" + }, + { + "label": "Easygoingp", + "value": "1157" + }, + { + "label": "Eccentricp", + "value": "1196" + }, + { + "label": "Extraordin", + "value": "1206" + }, + { + "label": "EatingBroa", + "value": "1238" + }, + { + "label": "Evil", + "value": "1289" + }, + { + "label": "entertainm", + "value": "1300" + }, + { + "label": "EvilSprits", + "value": "1313" + }, + { + "label": "exes", + "value": "1368" + }, + { + "label": "electricia", + "value": "1394" + }, + { + "label": "EmpireBuil", + "value": "1495" + }, + { + "label": "Education", + "value": "1504" + }, + { + "label": "EldestSist", + "value": "1508" + }, + { + "label": "Entertainm", + "value": "1521" + }, + { + "label": "EunuchJinr", + "value": "1681" + }, + { + "label": "Elfcold", + "value": "1758" + }, + { + "label": "EmperorYao", + "value": "1789" + }, + { + "label": "Eggpie", + "value": "2045" + }, + { + "label": "Extremelyi", + "value": "2100" + }, + { + "label": "Evergrande", + "value": "2210" + }, + { + "label": "emptymonol", + "value": "2261" + }, + { + "label": "eternityor", + "value": "2312" + }, + { + "label": "Entertaini", + "value": "2313" + }, + { + "label": "EmperorCha", + "value": "2363" + }, + { + "label": "EndoftheWo", + "value": "2378" + }, + { + "label": "everydayfi", + "value": "2383" + }, + { + "label": "entertainm", + "value": "2477" + }, + { + "label": "electricmo", + "value": "2495" + }, + { + "label": "engageinba", + "value": "2545" + }, + { + "label": "everlastin", + "value": "2550" + }, + { + "label": "Erwazi", + "value": "2658" + }, + { + "label": "entertainm", + "value": "2664" + }, + { + "label": "Eggplantan", + "value": "2673" + }, + { + "label": "Eighteence", + "value": "2781" + }, + { + "label": "eartwarmin", + "value": "2834" + }, + { + "label": "Emperor", + "value": "2844" + }, + { + "label": "Emotional", + "value": "2894" + }, + { + "label": "elemental", + "value": "2921" + }, + { + "label": "empressfem", + "value": "2947" + }, + { + "label": "evenge", + "value": "2969" + }, + { + "label": "EvilSpirit", + "value": "3015" + }, + { + "label": "Ex", + "value": "3030" + }, + { + "label": "Excessivel", + "value": "3034" + }, + { + "label": "Easternmys", + "value": "3077" + }, + { + "label": "EvilAuthor", + "value": "3095" + }, + { + "label": "Encryption", + "value": "3102" + }, + { + "label": "Eastern", + "value": "3104" + }, + { + "label": "Elements", + "value": "3105" + }, + { + "label": "exercise", + "value": "3122" + }, + { + "label": "Egoism", + "value": "3124" + }, + { + "label": "Enemies", + "value": "3128" + }, + { + "label": "Esper", + "value": "3223" + }, + { + "label": "ElderlyPro", + "value": "3250" + }, + { + "label": "Faloo", + "value": "640" + }, + { + "label": "FemaleProt", + "value": "45" + }, + { + "label": "Fan-fictio", + "value": "115" + }, + { + "label": "Fantasy", + "value": "240" + }, + { + "label": "Fanfiction", + "value": "86" + }, + { + "label": "Farming", + "value": "102" + }, + { + "label": "Futuristic", + "value": "199" + }, + { + "label": "fanqienove", + "value": "2942" + }, + { + "label": "FastCultiv", + "value": "70" + }, + { + "label": "FantasyWor", + "value": "47" + }, + { + "label": "Family", + "value": "228" + }, + { + "label": "FamilialLo", + "value": "101" + }, + { + "label": "Friendship", + "value": "392" + }, + { + "label": "FirstLove", + "value": "504" + }, + { + "label": "FamousProt", + "value": "249" + }, + { + "label": "FamilyConf", + "value": "548" + }, + { + "label": "FastLearne", + "value": "71" + }, + { + "label": "FaceSlappi", + "value": "739" + }, + { + "label": "FatedLover", + "value": "442" + }, + { + "label": "Football", + "value": "266" + }, + { + "label": "FantasyMag", + "value": "310" + }, + { + "label": "Firearms", + "value": "330" + }, + { + "label": "FantasyCre", + "value": "374" + }, + { + "label": "FamilyBusi", + "value": "248" + }, + { + "label": "ForcedMarr", + "value": "573" + }, + { + "label": "Fellatio", + "value": "433" + }, + { + "label": "FairyTail", + "value": "646" + }, + { + "label": "FutureCivi", + "value": "135" + }, + { + "label": "Fastpaced", + "value": "3094" + }, + { + "label": "FattoFit", + "value": "582" + }, + { + "label": "Fanfic", + "value": "718" + }, + { + "label": "FemaleMast", + "value": "588" + }, + { + "label": "FoxSpirits", + "value": "712" + }, + { + "label": "First-time", + "value": "747" + }, + { + "label": "FoodWars!", + "value": "663" + }, + { + "label": "FamousPare", + "value": "335" + }, + { + "label": "FengShui", + "value": "420" + }, + { + "label": "Future", + "value": "1287" + }, + { + "label": "Flashbacks", + "value": "1079" + }, + { + "label": "Finance", + "value": "1540" + }, + { + "label": "FearlessPr", + "value": "434" + }, + { + "label": "FemaleLead", + "value": "678" + }, + { + "label": "FallenNobi", + "value": "297" + }, + { + "label": "Fairies", + "value": "726" + }, + { + "label": "FatProtago", + "value": "998" + }, + { + "label": "Faceslap", + "value": "1226" + }, + { + "label": "Fatedlove", + "value": "3006" + }, + { + "label": "FemaletoMa", + "value": "1329" + }, + { + "label": "Feelgood", + "value": "2998" + }, + { + "label": "FriendsBec", + "value": "562" + }, + { + "label": "Food", + "value": "972" + }, + { + "label": "Forbiddenl", + "value": "3023" + }, + { + "label": "Fujoshi", + "value": "72" + }, + { + "label": "FleetBattl", + "value": "569" + }, + { + "label": "Fusión", + "value": "810" + }, + { + "label": "FallenAnge", + "value": "1441" + }, + { + "label": "Familiars", + "value": "1443" + }, + { + "label": "Folklore", + "value": "1417" + }, + { + "label": "futureworl", + "value": "2773" + }, + { + "label": "Fanaticism", + "value": "576" + }, + { + "label": "Femaleprot", + "value": "744" + }, + { + "label": "Futanari", + "value": "922" + }, + { + "label": "Formations", + "value": "926" + }, + { + "label": "Fishing", + "value": "1085" + }, + { + "label": "Farm", + "value": "1254" + }, + { + "label": "famouscoup", + "value": "1263" + }, + { + "label": "FormerHero", + "value": "1470" + }, + { + "label": "FamilyBuil", + "value": "1592" + }, + { + "label": "FengziXiao", + "value": "1925" + }, + { + "label": "Femaleside", + "value": "2970" + }, + { + "label": "Friendstol", + "value": "3025" + }, + { + "label": "FemaleMast", + "value": "339" + }, + { + "label": "ForgetfulP", + "value": "467" + }, + { + "label": "First-time", + "value": "491" + }, + { + "label": "FutureCivi", + "value": "771" + }, + { + "label": "FourthDisa", + "value": "801" + }, + { + "label": "FemalesPro", + "value": "900" + }, + { + "label": "FamillialL", + "value": "917" + }, + { + "label": "FarmingTex", + "value": "928" + }, + { + "label": "FemaleMC", + "value": "1003" + }, + { + "label": "FemalePres", + "value": "1023" + }, + { + "label": "FastWearin", + "value": "1035" + }, + { + "label": "FemaleSpie", + "value": "1074" + }, + { + "label": "France", + "value": "1080" + }, + { + "label": "Futuristic", + "value": "1087" + }, + { + "label": "ForcedLivi", + "value": "1141" + }, + { + "label": "FanFicton", + "value": "1144" + }, + { + "label": "FateSeries", + "value": "1186" + }, + { + "label": "FemaleFigh", + "value": "1187" + }, + { + "label": "familylife", + "value": "1192" + }, + { + "label": "FastGrowth", + "value": "1219" + }, + { + "label": "FamilyLove", + "value": "1270" + }, + { + "label": "Fantasyfut", + "value": "1369" + }, + { + "label": "Forcedinto", + "value": "1424" + }, + { + "label": "Famous", + "value": "1522" + }, + { + "label": "Fistfights", + "value": "1541" + }, + { + "label": "Friction", + "value": "1569" + }, + { + "label": "FaketoReal", + "value": "1582" + }, + { + "label": "Firethief", + "value": "1655" + }, + { + "label": "Fairy丨Pin", + "value": "1673" + }, + { + "label": "foxlisteni", + "value": "1680" + }, + { + "label": "flamingfla", + "value": "1693" + }, + { + "label": "Friday", + "value": "1744" + }, + { + "label": "Famousdete", + "value": "1769" + }, + { + "label": "FerrariEnz", + "value": "1779" + }, + { + "label": "FeiLuEdiso", + "value": "1796" + }, + { + "label": "FallingRai", + "value": "1798" + }, + { + "label": "FairySword", + "value": "1822" + }, + { + "label": "firstperso", + "value": "1832" + }, + { + "label": "Fireinthes", + "value": "1891" + }, + { + "label": "fatmanoffa", + "value": "1901" + }, + { + "label": "fishfishda", + "value": "1927" + }, + { + "label": "Followthew", + "value": "1941" + }, + { + "label": "Fallenleav", + "value": "1959" + }, + { + "label": "Favoritebl", + "value": "1965" + }, + { + "label": "fierce", + "value": "1984" + }, + { + "label": "forest", + "value": "1988" + }, + { + "label": "flyingfish", + "value": "2003" + }, + { + "label": "fullmeal", + "value": "2030" + }, + { + "label": "Forgiveyou", + "value": "2068" + }, + { + "label": "Fahaiunder", + "value": "2105" + }, + { + "label": "FantaCola", + "value": "2122" + }, + { + "label": "FanJiu", + "value": "2128" + }, + { + "label": "FlyingLuTi", + "value": "2158" + }, + { + "label": "furioussna", + "value": "2188" + }, + { + "label": "FoxdemonXi", + "value": "2226" + }, + { + "label": "flyingsqui", + "value": "2227" + }, + { + "label": "FangQingya", + "value": "2233" + }, + { + "label": "FireWinged", + "value": "2241" + }, + { + "label": "Fengqing", + "value": "2282" + }, + { + "label": "FifthEmper", + "value": "2306" + }, + { + "label": "Fourkeys", + "value": "2310" + }, + { + "label": "FightingCo", + "value": "2311" + }, + { + "label": "fairygirlf", + "value": "2327" + }, + { + "label": "fishandraf", + "value": "2336" + }, + { + "label": "Fantasybos", + "value": "2373" + }, + { + "label": "flyinglitt", + "value": "2398" + }, + { + "label": "firstgreen", + "value": "2409" + }, + { + "label": "flyingcow", + "value": "2424" + }, + { + "label": "Floatingli", + "value": "2427" + }, + { + "label": "fakegod", + "value": "2432" + }, + { + "label": "fisheatpan", + "value": "2443" + }, + { + "label": "FatDiddy", + "value": "2467" + }, + { + "label": "fireonfire", + "value": "2476" + }, + { + "label": "flyinthelo", + "value": "2492" + }, + { + "label": "FahaiInvin", + "value": "2537" + }, + { + "label": "Flyingwhit", + "value": "2553" + }, + { + "label": "Faucet", + "value": "2588" + }, + { + "label": "fanofstar", + "value": "2599" + }, + { + "label": "flyingshar", + "value": "2618" + }, + { + "label": "fishinflam", + "value": "2632" + }, + { + "label": "Favoriteco", + "value": "2642" + }, + { + "label": "FallenWing", + "value": "2644" + }, + { + "label": "fishswimmi", + "value": "2657" + }, + { + "label": "flowersoft", + "value": "2685" + }, + { + "label": "Fifi&03", + "value": "2689" + }, + { + "label": "fallintoth", + "value": "2690" + }, + { + "label": "Fishheadis", + "value": "2696" + }, + { + "label": "formworksk", + "value": "2701" + }, + { + "label": "FemaleProt", + "value": "2770" + }, + { + "label": "Fairy", + "value": "2823" + }, + { + "label": "Fullcolor", + "value": "2862" + }, + { + "label": "FemaleEmpe", + "value": "2880" + }, + { + "label": "Formation", + "value": "2884" + }, + { + "label": "Funny", + "value": "2901" + }, + { + "label": "FantasyCre", + "value": "2915" + }, + { + "label": "FemalePart", + "value": "2925" + }, + { + "label": "fasttravel", + "value": "2927" + }, + { + "label": "Fightforhe", + "value": "2929" + }, + { + "label": "futuredyst", + "value": "2949" + }, + { + "label": "Familyreun", + "value": "3008" + }, + { + "label": "Fakeandrea", + "value": "3032" + }, + { + "label": "Fantasia", + "value": "3056" + }, + { + "label": "Fantasyrom", + "value": "3106" + }, + { + "label": "Fastpace", + "value": "3191" + }, + { + "label": "Foursome", + "value": "3201" + }, + { + "label": "Fetish", + "value": "3207" + }, + { + "label": "Fiction", + "value": "3220" + }, + { + "label": "fqloo", + "value": "3236" + }, + { + "label": "GameElemen", + "value": "73" + }, + { + "label": "GeniusProt", + "value": "159" + }, + { + "label": "Ghosts", + "value": "311" + }, + { + "label": "Gods", + "value": "167" + }, + { + "label": "Gamers", + "value": "160" + }, + { + "label": "GodProtago", + "value": "136" + }, + { + "label": "GodlyPower", + "value": "137" + }, + { + "label": "genius", + "value": "1343" + }, + { + "label": "GatetoAnot", + "value": "109" + }, + { + "label": "GameRankin", + "value": "241" + }, + { + "label": "Generals", + "value": "256" + }, + { + "label": "GeneticMod", + "value": "422" + }, + { + "label": "Gore", + "value": "303" + }, + { + "label": "Game", + "value": "937" + }, + { + "label": "Guilds", + "value": "482" + }, + { + "label": "Goddesses", + "value": "166" + }, + { + "label": "Gangs", + "value": "299" + }, + { + "label": "Gunfighter", + "value": "559" + }, + { + "label": "GeneModifi", + "value": "661" + }, + { + "label": "GamingE-Sp", + "value": "496" + }, + { + "label": "GenderBend", + "value": "872" + }, + { + "label": "Goblins", + "value": "627" + }, + { + "label": "GameElemen", + "value": "843" + }, + { + "label": "Gambling", + "value": "461" + }, + { + "label": "God", + "value": "839" + }, + { + "label": "Grinding", + "value": "331" + }, + { + "label": "Gangsters", + "value": "1295" + }, + { + "label": "GoldenFing", + "value": "769" + }, + { + "label": "GuardianRe", + "value": "1272" + }, + { + "label": "Gaming", + "value": "1121" + }, + { + "label": "Genies", + "value": "200" + }, + { + "label": "GenshinImp", + "value": "668" + }, + { + "label": "Ghost", + "value": "813" + }, + { + "label": "GameRangki", + "value": "982" + }, + { + "label": "Golems", + "value": "1291" + }, + { + "label": "Grimdark", + "value": "631" + }, + { + "label": "GameOnline", + "value": "1013" + }, + { + "label": "Gourmet", + "value": "1150" + }, + { + "label": "Glasses-we", + "value": "1341" + }, + { + "label": "Gundam", + "value": "1353" + }, + { + "label": "Gettingbac", + "value": "3029" + }, + { + "label": "Grouppampe", + "value": "3031" + }, + { + "label": "Global", + "value": "3162" + }, + { + "label": "Genderless", + "value": "361" + }, + { + "label": "God-humanR", + "value": "465" + }, + { + "label": "GreedyProt", + "value": "714" + }, + { + "label": "GroupChat", + "value": "799" + }, + { + "label": "gameworld", + "value": "953" + }, + { + "label": "GodLikeMC", + "value": "1006" + }, + { + "label": "galacticli", + "value": "1043" + }, + { + "label": "Grupchat", + "value": "1054" + }, + { + "label": "Growth", + "value": "1066" + }, + { + "label": "GodandDevi", + "value": "1097" + }, + { + "label": "Genshin", + "value": "1099" + }, + { + "label": "Goddess", + "value": "1177" + }, + { + "label": "GodlyPower", + "value": "1183" + }, + { + "label": "Geass", + "value": "1191" + }, + { + "label": "Government", + "value": "1222" + }, + { + "label": "gameelemen", + "value": "1234" + }, + { + "label": "Genderless", + "value": "1239" + }, + { + "label": "Giants", + "value": "1288" + }, + { + "label": "Godzilla", + "value": "1311" + }, + { + "label": "GetRich", + "value": "1319" + }, + { + "label": "GentleProt", + "value": "1324" + }, + { + "label": "GentleLove", + "value": "1366" + }, + { + "label": "Glasses-we", + "value": "1453" + }, + { + "label": "GenderRole", + "value": "1510" + }, + { + "label": "Genderbend", + "value": "1512" + }, + { + "label": "GameSystem", + "value": "1524" + }, + { + "label": "Guns", + "value": "1542" + }, + { + "label": "GodlyProta", + "value": "1591" + }, + { + "label": "Galge", + "value": "1618" + }, + { + "label": "greentea", + "value": "1629" + }, + { + "label": "GradeXNUMX", + "value": "1649" + }, + { + "label": "GuShaoxia", + "value": "1707" + }, + { + "label": "goslowbro", + "value": "1712" + }, + { + "label": "goodpotdre", + "value": "1721" + }, + { + "label": "godofduel", + "value": "1760" + }, + { + "label": "Galacticos", + "value": "1771" + }, + { + "label": "goldfinger", + "value": "1792" + }, + { + "label": "Go", + "value": "1793" + }, + { + "label": "gentleman", + "value": "1834" + }, + { + "label": "GodofForti", + "value": "1848" + }, + { + "label": "GreatSage", + "value": "1880" + }, + { + "label": "gossip", + "value": "1906" + }, + { + "label": "giveyoutim", + "value": "1913" + }, + { + "label": "GoneStrawb", + "value": "1928" + }, + { + "label": "Gotaki", + "value": "1942" + }, + { + "label": "God&039", + "value": "2027" + }, + { + "label": "Galaxyboy", + "value": "2107" + }, + { + "label": "GreatCeles", + "value": "2174" + }, + { + "label": "GLL", + "value": "2185" + }, + { + "label": "Godofwings", + "value": "2186" + }, + { + "label": "goddessbos", + "value": "2238" + }, + { + "label": "Ghostsinre", + "value": "2263" + }, + { + "label": "Goddidnotg", + "value": "2271" + }, + { + "label": "Gooifyouca", + "value": "2349" + }, + { + "label": "GuiltyScis", + "value": "2455" + }, + { + "label": "godsaltedf", + "value": "2463" + }, + { + "label": "Golden", + "value": "2504" + }, + { + "label": "good-natur", + "value": "2528" + }, + { + "label": "GaoYuanyao", + "value": "2541" + }, + { + "label": "goallist", + "value": "2544" + }, + { + "label": "Ghostexter", + "value": "2560" + }, + { + "label": "goldenfore", + "value": "2630" + }, + { + "label": "GeneralXie", + "value": "2653" + }, + { + "label": "giantpanda", + "value": "2719" + }, + { + "label": "GroupPet", + "value": "2771" + }, + { + "label": "GingerLemo", + "value": "2803" + }, + { + "label": "gongregret", + "value": "2871" + }, + { + "label": "goldrush", + "value": "2893" + }, + { + "label": "Guideverse", + "value": "2946" + }, + { + "label": "Groupspoil", + "value": "3007" + }, + { + "label": "Goodvibes", + "value": "3040" + }, + { + "label": "Genetic", + "value": "3098" + }, + { + "label": "Genderneut", + "value": "3114" + }, + { + "label": "Giantdrago", + "value": "3164" + }, + { + "label": "GeniusFema", + "value": "3168" + }, + { + "label": "Gilf", + "value": "3203" + }, + { + "label": "gacha", + "value": "3218" + }, + { + "label": "HandsomeMa", + "value": "11" + }, + { + "label": "Harem", + "value": "21" + }, + { + "label": "HidingTrue", + "value": "191" + }, + { + "label": "Heartwarmi", + "value": "57" + }, + { + "label": "HarryPotte", + "value": "436" + }, + { + "label": "HidingTrue", + "value": "39" + }, + { + "label": "HiddenAbil", + "value": "190" + }, + { + "label": "Historical", + "value": "641" + }, + { + "label": "Heroes", + "value": "462" + }, + { + "label": "Hackers", + "value": "188" + }, + { + "label": "Hunters", + "value": "500" + }, + { + "label": "Highiq", + "value": "2981" + }, + { + "label": "HumanExper", + "value": "312" + }, + { + "label": "HumanoidPr", + "value": "528" + }, + { + "label": "HeavenlyTr", + "value": "87" + }, + { + "label": "Hero", + "value": "624" + }, + { + "label": "horror", + "value": "699" + }, + { + "label": "HunterxHun", + "value": "692" + }, + { + "label": "HiddenTrue", + "value": "745" + }, + { + "label": "HonestProt", + "value": "903" + }, + { + "label": "Hell", + "value": "543" + }, + { + "label": "Healers", + "value": "577" + }, + { + "label": "HatedProta", + "value": "1407" + }, + { + "label": "Hunter", + "value": "2837" + }, + { + "label": "Handjob", + "value": "539" + }, + { + "label": "Healing", + "value": "1049" + }, + { + "label": "Hospital", + "value": "439" + }, + { + "label": "HidingTrue", + "value": "855" + }, + { + "label": "Heaven", + "value": "938" + }, + { + "label": "Hacker", + "value": "1091" + }, + { + "label": "HiddenIden", + "value": "1180" + }, + { + "label": "HiddenGem", + "value": "2971" + }, + { + "label": "Horor", + "value": "3226" + }, + { + "label": "HumanWeapo", + "value": "427" + }, + { + "label": "HandsomePr", + "value": "762" + }, + { + "label": "Hypnotism", + "value": "860" + }, + { + "label": "Heterochro", + "value": "1262" + }, + { + "label": "HelpfulPro", + "value": "503" + }, + { + "label": "Hollywood", + "value": "909" + }, + { + "label": "HonkaiImpa", + "value": "976" + }, + { + "label": "History", + "value": "1496" + }, + { + "label": "HiddenIden", + "value": "696" + }, + { + "label": "HarshTrain", + "value": "952" + }, + { + "label": "HxH", + "value": "956" + }, + { + "label": "Hogwarts", + "value": "1211" + }, + { + "label": "Herbalist", + "value": "1459" + }, + { + "label": "HappyEndin", + "value": "1477" + }, + { + "label": "HotBlood", + "value": "2795" + }, + { + "label": "Hiddenmarr", + "value": "3010" + }, + { + "label": "Humour", + "value": "3109" + }, + { + "label": "Hard-Worki", + "value": "103" + }, + { + "label": "Harem-seek", + "value": "138" + }, + { + "label": "Human-Nonh", + "value": "321" + }, + { + "label": "Hot-bloode", + "value": "426" + }, + { + "label": "Happy", + "value": "865" + }, + { + "label": "HighSchool", + "value": "879" + }, + { + "label": "Hardworkin", + "value": "881" + }, + { + "label": "HumanExper", + "value": "1004" + }, + { + "label": "HiddenBoss", + "value": "1063" + }, + { + "label": "HaremSeeki", + "value": "1176" + }, + { + "label": "HandsomeMa", + "value": "1256" + }, + { + "label": "Haikyuu", + "value": "1393" + }, + { + "label": "Half-human", + "value": "1406" + }, + { + "label": "HidingAbil", + "value": "1471" + }, + { + "label": "Halo", + "value": "1473" + }, + { + "label": "Hokage", + "value": "1490" + }, + { + "label": "HidingTrue", + "value": "1584" + }, + { + "label": "heroine", + "value": "1630" + }, + { + "label": "HongmengSh", + "value": "1634" + }, + { + "label": "HolyKingRa", + "value": "1676" + }, + { + "label": "hunterkill", + "value": "1734" + }, + { + "label": "HongTang", + "value": "1735" + }, + { + "label": "hyperknigh", + "value": "1778" + }, + { + "label": "Heroesofth", + "value": "1808" + }, + { + "label": "hi", + "value": "1844" + }, + { + "label": "HuiMochou", + "value": "1847" + }, + { + "label": "holyangel", + "value": "1861" + }, + { + "label": "HuanHuanHu", + "value": "1909" + }, + { + "label": "Hashihime", + "value": "1948" + }, + { + "label": "hey", + "value": "1950" + }, + { + "label": "Higu", + "value": "1963" + }, + { + "label": "HappyBeanl", + "value": "2032" + }, + { + "label": "酣歌", + "value": "2034" + }, + { + "label": "Honghuangs", + "value": "2073" + }, + { + "label": "humla", + "value": "2075" + }, + { + "label": "Huijingund", + "value": "2102" + }, + { + "label": "howlingpig", + "value": "2164" + }, + { + "label": "Healthewor", + "value": "2182" + }, + { + "label": "Hawkeye", + "value": "2189" + }, + { + "label": "HaotianExt", + "value": "2219" + }, + { + "label": "handtearin", + "value": "2223" + }, + { + "label": "HomeAttrib", + "value": "2234" + }, + { + "label": "HuTiandi", + "value": "2235" + }, + { + "label": "horrorgod", + "value": "2247" + }, + { + "label": "HakoniwaSe", + "value": "2255" + }, + { + "label": "houseprope", + "value": "2275" + }, + { + "label": "HisMajesty", + "value": "2283" + }, + { + "label": "halfstepge", + "value": "2345" + }, + { + "label": "HonghuangN", + "value": "2392" + }, + { + "label": "Haremismta", + "value": "2401" + }, + { + "label": "heavenclea", + "value": "2405" + }, + { + "label": "heavensong", + "value": "2420" + }, + { + "label": "HongfeiQin", + "value": "2480" + }, + { + "label": "handsomeon", + "value": "2487" + }, + { + "label": "halfanoran", + "value": "2491" + }, + { + "label": "heartandey", + "value": "2494" + }, + { + "label": "HonestandR", + "value": "2507" + }, + { + "label": "HeartHunte", + "value": "2556" + }, + { + "label": "Haminstant", + "value": "2563" + }, + { + "label": "hotpot", + "value": "2611" + }, + { + "label": "H11H", + "value": "2613" + }, + { + "label": "howlingwin", + "value": "2655" + }, + { + "label": "Handsomegu", + "value": "2671" + }, + { + "label": "HappyFlow", + "value": "2702" + }, + { + "label": "Hegemony", + "value": "2763" + }, + { + "label": "Hunter×Hu", + "value": "2780" + }, + { + "label": "hitten", + "value": "2791" + }, + { + "label": "HaoyuYingx", + "value": "2804" + }, + { + "label": "HeartBreak", + "value": "2895" + }, + { + "label": "Historical", + "value": "2966" + }, + { + "label": "Heartthrob", + "value": "3038" + }, + { + "label": "Historical", + "value": "3064" + }, + { + "label": "HiddenWeap", + "value": "3097" + }, + { + "label": "Hentai", + "value": "3192" + }, + { + "label": "Highschool", + "value": "3194" + }, + { + "label": "Humiliatio", + "value": "3206" + }, + { + "label": "HandsomeMC", + "value": "3209" + }, + { + "label": "Immortals", + "value": "260" + }, + { + "label": "Isekai", + "value": "983" + }, + { + "label": "ImperialHa", + "value": "344" + }, + { + "label": "Industrial", + "value": "560" + }, + { + "label": "Interstell", + "value": "710" + }, + { + "label": "Incest", + "value": "453" + }, + { + "label": "Invincible", + "value": "3074" + }, + { + "label": "Inheritanc", + "value": "229" + }, + { + "label": "Immortal", + "value": "1045" + }, + { + "label": "Insects", + "value": "244" + }, + { + "label": "Interestel", + "value": "698" + }, + { + "label": "Investigat", + "value": "1154" + }, + { + "label": "Inferiorit", + "value": "262" + }, + { + "label": "Infrastruc", + "value": "989" + }, + { + "label": "IdentityCr", + "value": "122" + }, + { + "label": "Inscriptio", + "value": "1413" + }, + { + "label": "Industry", + "value": "818" + }, + { + "label": "InfiniteFl", + "value": "822" + }, + { + "label": "Interdimen", + "value": "423" + }, + { + "label": "Idol", + "value": "845" + }, + { + "label": "Investigat", + "value": "885" + }, + { + "label": "infinite", + "value": "966" + }, + { + "label": "InnerVoice", + "value": "1242" + }, + { + "label": "ImperialFa", + "value": "1609" + }, + { + "label": "Indecisive", + "value": "359" + }, + { + "label": "Introverte", + "value": "511" + }, + { + "label": "infrastrac", + "value": "975" + }, + { + "label": "imperialco", + "value": "1048" + }, + { + "label": "Illigitima", + "value": "1315" + }, + { + "label": "Interconne", + "value": "1360" + }, + { + "label": "Investigat", + "value": "1513" + }, + { + "label": "Intelligen", + "value": "1534" + }, + { + "label": "Ilo", + "value": "1639" + }, + { + "label": "Invincible", + "value": "1660" + }, + { + "label": "idropbaby", + "value": "1686" + }, + { + "label": "Invincible", + "value": "1723" + }, + { + "label": "Invincible", + "value": "1742" + }, + { + "label": "Iamtheseak", + "value": "1751" + }, + { + "label": "Ijustwantt", + "value": "1757" + }, + { + "label": "iamatravel", + "value": "1781" + }, + { + "label": "Invincible", + "value": "1810" + }, + { + "label": "icewalk", + "value": "1824" + }, + { + "label": "Intercept0", + "value": "1842" + }, + { + "label": "Iliveupsta", + "value": "1863" + }, + { + "label": "Iamnotaloc", + "value": "1865" + }, + { + "label": "Infiniteme", + "value": "1883" + }, + { + "label": "icalledthe", + "value": "1888" + }, + { + "label": "isitnecess", + "value": "1921" + }, + { + "label": "Infernalco", + "value": "1924" + }, + { + "label": "Isuckbrown", + "value": "1949" + }, + { + "label": "Itsdaybrea", + "value": "1962" + }, + { + "label": "Iwishyouat", + "value": "1972" + }, + { + "label": "Ifyoucango", + "value": "1973" + }, + { + "label": "It&039s", + "value": "2039" + }, + { + "label": "I&039ma", + "value": "2041" + }, + { + "label": "idon&03", + "value": "2049" + }, + { + "label": "InfiniteBu", + "value": "2053" + }, + { + "label": "Iamolderth", + "value": "2125" + }, + { + "label": "IamHisMaje", + "value": "2151" + }, + { + "label": "Iamarealdi", + "value": "2166" + }, + { + "label": "Iamfifth", + "value": "2172" + }, + { + "label": "Iamapirate", + "value": "2175" + }, + { + "label": "ironpillar", + "value": "2187" + }, + { + "label": "I&039mo", + "value": "2208" + }, + { + "label": "IamGuanxi", + "value": "2286" + }, + { + "label": "Iamhell", + "value": "2300" + }, + { + "label": "iwantmoney", + "value": "2315" + }, + { + "label": "IsumiLily", + "value": "2347" + }, + { + "label": "Iwanttobea", + "value": "2360" + }, + { + "label": "Iwanttobeo", + "value": "2402" + }, + { + "label": "infinitesu", + "value": "2412" + }, + { + "label": "Ibuprofen", + "value": "2456" + }, + { + "label": "Iamtwenty-", + "value": "2479" + }, + { + "label": "ieatgrass", + "value": "2485" + }, + { + "label": "iwanttogot", + "value": "2514" + }, + { + "label": "Iamtheseco", + "value": "2527" + }, + { + "label": "ImmortalBi", + "value": "2534" + }, + { + "label": "IronThanos", + "value": "2547" + }, + { + "label": "Iamoldwolf", + "value": "2575" + }, + { + "label": "ilovewoo", + "value": "2585" + }, + { + "label": "IamAsi", + "value": "2625" + }, + { + "label": "ihavethere", + "value": "2650" + }, + { + "label": "Iamthemurd", + "value": "2652" + }, + { + "label": "ImmortalMa", + "value": "2660" + }, + { + "label": "Ink", + "value": "2681" + }, + { + "label": "Intercept0", + "value": "2684" + }, + { + "label": "insitu", + "value": "2693" + }, + { + "label": "iwanttoeat", + "value": "2697" + }, + { + "label": "IcedDurian", + "value": "2707" + }, + { + "label": "Inuyasha", + "value": "2749" + }, + { + "label": "IronMaiden", + "value": "2784" + }, + { + "label": "Iateeightc", + "value": "2787" + }, + { + "label": "Incubus", + "value": "2810" + }, + { + "label": "ImperialEx", + "value": "2818" + }, + { + "label": "industrial", + "value": "2835" + }, + { + "label": "Inferior", + "value": "2896" + }, + { + "label": "industryel", + "value": "2953" + }, + { + "label": "Imposter", + "value": "2980" + }, + { + "label": "Invincibil", + "value": "3148" + }, + { + "label": "Issekai", + "value": "3182" + }, + { + "label": "Jealousy", + "value": "551" + }, + { + "label": "JackofAllT", + "value": "75" + }, + { + "label": "Journeytot", + "value": "677" + }, + { + "label": "Josei", + "value": "876" + }, + { + "label": "JujutsuKai", + "value": "675" + }, + { + "label": "Jiangshi", + "value": "406" + }, + { + "label": "JoJo", + "value": "2740" + }, + { + "label": "Jianghu", + "value": "786" + }, + { + "label": "Japan", + "value": "853" + }, + { + "label": "JangSeok-g", + "value": "1642" + }, + { + "label": "Juliet", + "value": "1658" + }, + { + "label": "JOJOWE", + "value": "1806" + }, + { + "label": "joydrummer", + "value": "1866" + }, + { + "label": "Jiutianyu", + "value": "2029" + }, + { + "label": "JuniorSist", + "value": "2037" + }, + { + "label": "jadeeveryy", + "value": "2198" + }, + { + "label": "Jianjiamix", + "value": "2249" + }, + { + "label": "jellyjelly", + "value": "2365" + }, + { + "label": "John117", + "value": "2508" + }, + { + "label": "Jazz", + "value": "2564" + }, + { + "label": "JinglongTa", + "value": "2643" + }, + { + "label": "JunCaiXing", + "value": "2670" + }, + { + "label": "justshout", + "value": "2676" + }, + { + "label": "JoJo&03", + "value": "2741" + }, + { + "label": "JackieChan", + "value": "2750" + }, + { + "label": "Judge", + "value": "3063" + }, + { + "label": "Jurassic", + "value": "3158" + }, + { + "label": "KingdomBui", + "value": "245" + }, + { + "label": "Kingdoms", + "value": "139" + }, + { + "label": "Knights", + "value": "389" + }, + { + "label": "KoreanNove", + "value": "652" + }, + { + "label": "KindLoveIn", + "value": "544" + }, + { + "label": "Kingdom-bu", + "value": "971" + }, + { + "label": "Kidnapping", + "value": "552" + }, + { + "label": "Killer", + "value": "2982" + }, + { + "label": "KnightsLev", + "value": "417" + }, + { + "label": "Kuudere", + "value": "1371" + }, + { + "label": "KingdomsKn", + "value": "317" + }, + { + "label": "KindProtag", + "value": "1031" + }, + { + "label": "Knight", + "value": "1559" + }, + { + "label": "King", + "value": "1745" + }, + { + "label": "Kojin", + "value": "2072" + }, + { + "label": "KindomBuil", + "value": "1027" + }, + { + "label": "Koi", + "value": "1181" + }, + { + "label": "Kingdom", + "value": "1236" + }, + { + "label": "KingdomBui", + "value": "1293" + }, + { + "label": "KpopIdols", + "value": "1337" + }, + { + "label": "Kindergart", + "value": "1370" + }, + { + "label": "Kakashi", + "value": "1491" + }, + { + "label": "K-popIdols", + "value": "1620" + }, + { + "label": "KingofDest", + "value": "1674" + }, + { + "label": "KnifePromi", + "value": "1852" + }, + { + "label": "KurongTemp", + "value": "1858" + }, + { + "label": "Kafkajumpi", + "value": "1887" + }, + { + "label": "KwunTong", + "value": "1898" + }, + { + "label": "kingpirate", + "value": "2044" + }, + { + "label": "Killthewor", + "value": "2056" + }, + { + "label": "Kneelingth", + "value": "2161" + }, + { + "label": "KingAsura", + "value": "2242" + }, + { + "label": "KingofMons", + "value": "2295" + }, + { + "label": "keytocome", + "value": "2323" + }, + { + "label": "Knowtheric", + "value": "2335" + }, + { + "label": "Kiritani", + "value": "2395" + }, + { + "label": "KonohaVoll", + "value": "2403" + }, + { + "label": "kingofdeat", + "value": "2468" + }, + { + "label": "KingKonggo", + "value": "2509" + }, + { + "label": "Kneelingan", + "value": "2678" + }, + { + "label": "Knightsand", + "value": "3067" + }, + { + "label": "Korean", + "value": "3103" + }, + { + "label": "LevelSyste", + "value": "123" + }, + { + "label": "Levelup", + "value": "2961" + }, + { + "label": "LuckyProta", + "value": "207" + }, + { + "label": "LoyalSubor", + "value": "341" + }, + { + "label": "LiveBroadc", + "value": "650" + }, + { + "label": "LateRomanc", + "value": "168" + }, + { + "label": "LazyProtag", + "value": "104" + }, + { + "label": "Leadership", + "value": "419" + }, + { + "label": "Low-keyPro", + "value": "22" + }, + { + "label": "LoveatFirs", + "value": "527" + }, + { + "label": "LackofComm", + "value": "340" + }, + { + "label": "Lolicon", + "value": "313" + }, + { + "label": "LoversReun", + "value": "443" + }, + { + "label": "Lawyers", + "value": "545" + }, + { + "label": "LonerProta", + "value": "364" + }, + { + "label": "LoveTriang", + "value": "848" + }, + { + "label": "LongSepara", + "value": "223" + }, + { + "label": "Lightnovel", + "value": "2959" + }, + { + "label": "LoveRivals", + "value": "948" + }, + { + "label": "Lottery", + "value": "572" + }, + { + "label": "Love", + "value": "1083" + }, + { + "label": "LordoftheM", + "value": "1112" + }, + { + "label": "Lovetriang", + "value": "3027" + }, + { + "label": "LivingAlon", + "value": "399" + }, + { + "label": "LowkeyProt", + "value": "960" + }, + { + "label": "Loli", + "value": "1483" + }, + { + "label": "Lovecomedy", + "value": "2940" + }, + { + "label": "LitRPG", + "value": "3112" + }, + { + "label": "LimitedLif", + "value": "285" + }, + { + "label": "ListCreati", + "value": "687" + }, + { + "label": "Legends", + "value": "140" + }, + { + "label": "Livestream", + "value": "700" + }, + { + "label": "LoveContra", + "value": "1178" + }, + { + "label": "LiveStream", + "value": "1207" + }, + { + "label": "LostCivili", + "value": "42" + }, + { + "label": "literature", + "value": "632" + }, + { + "label": "Luck", + "value": "1182" + }, + { + "label": "LuckPlunde", + "value": "1322" + }, + { + "label": "LifeScript", + "value": "1323" + }, + { + "label": "LordAbilit", + "value": "1328" + }, + { + "label": "Legend", + "value": "1451" + }, + { + "label": "Library", + "value": "1464" + }, + { + "label": "Lord", + "value": "1536" + }, + { + "label": "LiWudi", + "value": "1725" + }, + { + "label": "Lillie", + "value": "2520" + }, + { + "label": "Loveafterm", + "value": "2977" + }, + { + "label": "Ldg", + "value": "3044" + }, + { + "label": "LoveIntere", + "value": "12" + }, + { + "label": "LimitlessF", + "value": "615" + }, + { + "label": "LoyalProta", + "value": "831" + }, + { + "label": "Liar", + "value": "888" + }, + { + "label": "LoveandMar", + "value": "990" + }, + { + "label": "LoliProtag", + "value": "1106" + }, + { + "label": "LordGodSpa", + "value": "1189" + }, + { + "label": "LoyalSurbo", + "value": "1202" + }, + { + "label": "LoveIntere", + "value": "1231" + }, + { + "label": "LoveIntere", + "value": "1260" + }, + { + "label": "Long-dista", + "value": "1461" + }, + { + "label": "LeagueofLe", + "value": "1489" + }, + { + "label": "LoveGrowth", + "value": "1532" + }, + { + "label": "LiveBroadc", + "value": "1578" + }, + { + "label": "LowKeyMc", + "value": "1615" + }, + { + "label": "lesstime", + "value": "1691" + }, + { + "label": "LuoTianyi", + "value": "1716" + }, + { + "label": "LikeaDrago", + "value": "1752" + }, + { + "label": "LikeaDrago", + "value": "1755" + }, + { + "label": "Loseafewpo", + "value": "1775" + }, + { + "label": "LeiJiedoes", + "value": "1782" + }, + { + "label": "LordoftheS", + "value": "1829" + }, + { + "label": "Lonelynota", + "value": "1838" + }, + { + "label": "LuoWei", + "value": "1846" + }, + { + "label": "littlehson", + "value": "1851" + }, + { + "label": "LonelyCity", + "value": "1859" + }, + { + "label": "LiverPigeo", + "value": "1877" + }, + { + "label": "littlezlov", + "value": "1884" + }, + { + "label": "littlemoon", + "value": "1893" + }, + { + "label": "LaoLaoXu", + "value": "1922" + }, + { + "label": "Lingran", + "value": "1939" + }, + { + "label": "LacquerNig", + "value": "1955" + }, + { + "label": "lazydevil", + "value": "1976" + }, + { + "label": "LuDehua", + "value": "1985" + }, + { + "label": "littledemo", + "value": "1990" + }, + { + "label": "littlefox", + "value": "2007" + }, + { + "label": "Lightnings", + "value": "2020" + }, + { + "label": "Lovesnacks", + "value": "2042" + }, + { + "label": "littleahxi", + "value": "2054" + }, + { + "label": "laurel", + "value": "2058" + }, + { + "label": "LoneCloudP", + "value": "2094" + }, + { + "label": "lackofboat", + "value": "2120" + }, + { + "label": "LongMengme", + "value": "2140" + }, + { + "label": "littlebrot", + "value": "2145" + }, + { + "label": "lemonandsi", + "value": "2149" + }, + { + "label": "LiMumu", + "value": "2155" + }, + { + "label": "LameHaoisa", + "value": "2162" + }, + { + "label": "littlesuns", + "value": "2176" + }, + { + "label": "LordZhangj", + "value": "2190" + }, + { + "label": "Leapeveryd", + "value": "2197" + }, + { + "label": "littledevi", + "value": "2211" + }, + { + "label": "littlefing", + "value": "2239" + }, + { + "label": "Longpigeon", + "value": "2245" + }, + { + "label": "LiuYujun", + "value": "2280" + }, + { + "label": "Leisurelys", + "value": "2289" + }, + { + "label": "langyalist", + "value": "2292" + }, + { + "label": "Longliveth", + "value": "2297" + }, + { + "label": "longlivemy", + "value": "2316" + }, + { + "label": "LY", + "value": "2324" + }, + { + "label": "lonelyboy", + "value": "2359" + }, + { + "label": "laborhonor", + "value": "2399" + }, + { + "label": "LuoXIV", + "value": "2407" + }, + { + "label": "littlewind", + "value": "2429" + }, + { + "label": "Longlivesa", + "value": "2435" + }, + { + "label": "LinZhengyi", + "value": "2450" + }, + { + "label": "LikeMeiAox", + "value": "2459" + }, + { + "label": "LordTiansh", + "value": "2488" + }, + { + "label": "Longliveth", + "value": "2531" + }, + { + "label": "LingshanIs", + "value": "2532" + }, + { + "label": "LinXiufigh", + "value": "2539" + }, + { + "label": "listentoth", + "value": "2590" + }, + { + "label": "lu11034363", + "value": "2612" + }, + { + "label": "LinBei", + "value": "2620" + }, + { + "label": "Lindentree", + "value": "2628" + }, + { + "label": "LuoYuqianq", + "value": "2633" + }, + { + "label": "LiJunhao", + "value": "2636" + }, + { + "label": "lovetease", + "value": "2666" + }, + { + "label": "littledete", + "value": "2687" + }, + { + "label": "Low-keylux", + "value": "2699" + }, + { + "label": "littlecray", + "value": "2703" + }, + { + "label": "lendmefive", + "value": "2718" + }, + { + "label": "littlewate", + "value": "2727" + }, + { + "label": "longlivedm", + "value": "2735" + }, + { + "label": "零充", + "value": "2783" + }, + { + "label": "LeiXunqing", + "value": "2798" + }, + { + "label": "LoyalSubor", + "value": "2814" + }, + { + "label": "lazy", + "value": "2827" + }, + { + "label": "LittleWhit", + "value": "2836" + }, + { + "label": "LaidBack", + "value": "2850" + }, + { + "label": "ListAdvent", + "value": "2863" + }, + { + "label": "LongStrip", + "value": "2864" + }, + { + "label": "Lucky", + "value": "2873" + }, + { + "label": "Life", + "value": "2897" + }, + { + "label": "Littlebun", + "value": "3073" + }, + { + "label": "Levelingup", + "value": "3088" + }, + { + "label": "Loneliness", + "value": "3248" + }, + { + "label": "MaleProtag", + "value": "23" + }, + { + "label": "Magic", + "value": "141" + }, + { + "label": "ModernDay", + "value": "3" + }, + { + "label": "Monsters", + "value": "161" + }, + { + "label": "Marvel", + "value": "342" + }, + { + "label": "ModernWorl", + "value": "276" + }, + { + "label": "Misunderst", + "value": "1009" + }, + { + "label": "Misunderst", + "value": "80" + }, + { + "label": "MultipleRe", + "value": "94" + }, + { + "label": "Marriage", + "value": "46" + }, + { + "label": "Mystery", + "value": "701" + }, + { + "label": "MagicalSpa", + "value": "79" + }, + { + "label": "Martialart", + "value": "51" + }, + { + "label": "Military", + "value": "13" + }, + { + "label": "ModernKnow", + "value": "48" + }, + { + "label": "MedicalKno", + "value": "114" + }, + { + "label": "Mpreg", + "value": "402" + }, + { + "label": "Modern", + "value": "414" + }, + { + "label": "MMORPG", + "value": "337" + }, + { + "label": "MonsterTam", + "value": "81" + }, + { + "label": "MultipleId", + "value": "412" + }, + { + "label": "MaleYander", + "value": "515" + }, + { + "label": "MagicBeast", + "value": "242" + }, + { + "label": "Mysterious", + "value": "332" + }, + { + "label": "modernfant", + "value": "2852" + }, + { + "label": "Mythology", + "value": "143" + }, + { + "label": "MultiplePO", + "value": "171" + }, + { + "label": "Movies", + "value": "250" + }, + { + "label": "MysterySol", + "value": "424" + }, + { + "label": "MoneyGrubb", + "value": "230" + }, + { + "label": "MythicalBe", + "value": "142" + }, + { + "label": "MartialSpi", + "value": "231" + }, + { + "label": "MatureProt", + "value": "383" + }, + { + "label": "Munchkin", + "value": "3219" + }, + { + "label": "Music", + "value": "251" + }, + { + "label": "MagicForma", + "value": "77" + }, + { + "label": "MarvelUniv", + "value": "343" + }, + { + "label": "Mecha", + "value": "618" + }, + { + "label": "MiddleAges", + "value": "3211" + }, + { + "label": "ManlyGayCo", + "value": "995" + }, + { + "label": "MagicalTec", + "value": "400" + }, + { + "label": "MultiplePr", + "value": "490" + }, + { + "label": "Mercenarie", + "value": "211" + }, + { + "label": "Mutations", + "value": "216" + }, + { + "label": "Management", + "value": "464" + }, + { + "label": "Maids", + "value": "531" + }, + { + "label": "MaletoFema", + "value": "563" + }, + { + "label": "Medieval", + "value": "590" + }, + { + "label": "Mature", + "value": "2824" + }, + { + "label": "MutatedCre", + "value": "533" + }, + { + "label": "Medicine", + "value": "614" + }, + { + "label": "Mafia", + "value": "1533" + }, + { + "label": "Myth", + "value": "3054" + }, + { + "label": "MobProtago", + "value": "409" + }, + { + "label": "Murders", + "value": "595" + }, + { + "label": "MaleLead", + "value": "790" + }, + { + "label": "Mutation", + "value": "856" + }, + { + "label": "MultipleTi", + "value": "411" + }, + { + "label": "Matriarchy", + "value": "715" + }, + { + "label": "Merchants", + "value": "1411" + }, + { + "label": "modernlove", + "value": "1632" + }, + { + "label": "Murder", + "value": "124" + }, + { + "label": "MindContro", + "value": "221" + }, + { + "label": "MyHeroAcad", + "value": "693" + }, + { + "label": "Models", + "value": "444" + }, + { + "label": "Masturbati", + "value": "366" + }, + { + "label": "ModernDays", + "value": "797" + }, + { + "label": "MultipleWo", + "value": "858" + }, + { + "label": "MartialArt", + "value": "1046" + }, + { + "label": "MaleProtag", + "value": "2759" + }, + { + "label": "Multiplele", + "value": "3111" + }, + { + "label": "Mythical", + "value": "592" + }, + { + "label": "MultipleCP", + "value": "766" + }, + { + "label": "Monster", + "value": "935" + }, + { + "label": "Multiverse", + "value": "1111" + }, + { + "label": "MaleMc", + "value": "1199" + }, + { + "label": "magicalgir", + "value": "1358" + }, + { + "label": "MuteCharac", + "value": "1410" + }, + { + "label": "MassiveHar", + "value": "1561" + }, + { + "label": "MrMo", + "value": "1652" + }, + { + "label": "Mercenary", + "value": "629" + }, + { + "label": "Mangaka", + "value": "1374" + }, + { + "label": "MagicWorld", + "value": "1625" + }, + { + "label": "mermaid", + "value": "2734" + }, + { + "label": "MingDynast", + "value": "2845" + }, + { + "label": "Medical", + "value": "2881" + }, + { + "label": "Marysue", + "value": "3033" + }, + { + "label": "Mysterious", + "value": "176" + }, + { + "label": "MonsterGir", + "value": "357" + }, + { + "label": "Monogamy", + "value": "910" + }, + { + "label": "Male-Lead", + "value": "940" + }, + { + "label": "MartialSpi", + "value": "962" + }, + { + "label": "Male-Prota", + "value": "980" + }, + { + "label": "MaleProtag", + "value": "988" + }, + { + "label": "Mob", + "value": "1185" + }, + { + "label": "multiplere", + "value": "1257" + }, + { + "label": "Mysterious", + "value": "1357" + }, + { + "label": "ModernRoma", + "value": "1378" + }, + { + "label": "Mystical", + "value": "1458" + }, + { + "label": "Meowingbig", + "value": "1938" + }, + { + "label": "MarvelKing", + "value": "1960" + }, + { + "label": "ModernLife", + "value": "2767" + }, + { + "label": "mysteries", + "value": "2840" + }, + { + "label": "Master-Dis", + "value": "24" + }, + { + "label": "Masochisti", + "value": "365" + }, + { + "label": "Marriageof", + "value": "401" + }, + { + "label": "MultipleRe", + "value": "508" + }, + { + "label": "MultipleTr", + "value": "509" + }, + { + "label": "Master-Ser", + "value": "518" + }, + { + "label": "Manipulati", + "value": "607" + }, + { + "label": "MaleMain-l", + "value": "722" + }, + { + "label": "Married", + "value": "735" + }, + { + "label": "Master-App", + "value": "740" + }, + { + "label": "MultiplePe", + "value": "743" + }, + { + "label": "Mage", + "value": "763" + }, + { + "label": "MultipleTr", + "value": "764" + }, + { + "label": "MultiplePo", + "value": "802" + }, + { + "label": "Maleprotag", + "value": "825" + }, + { + "label": "Mismatched", + "value": "847" + }, + { + "label": "Mutualcrus", + "value": "874" + }, + { + "label": "MultipleMo", + "value": "882" + }, + { + "label": "MoneyGrumb", + "value": "907" + }, + { + "label": "MultipleHi", + "value": "911" + }, + { + "label": "MutantPowe", + "value": "929" + }, + { + "label": "ModerDays", + "value": "967" + }, + { + "label": "MultipleBo", + "value": "1005" + }, + { + "label": "Middleage", + "value": "1011" + }, + { + "label": "MagicalAbi", + "value": "1017" + }, + { + "label": "Millionair", + "value": "1024" + }, + { + "label": "MultipleVe", + "value": "1040" + }, + { + "label": "Manipulati", + "value": "1057" + }, + { + "label": "MindReader", + "value": "1064" + }, + { + "label": "MorallyAmb", + "value": "1075" + }, + { + "label": "MCStrongFr", + "value": "1098" + }, + { + "label": "MemoryLoss", + "value": "1107" + }, + { + "label": "MarriageCo", + "value": "1138" + }, + { + "label": "Maid", + "value": "1160" + }, + { + "label": "Misunderst", + "value": "1165" + }, + { + "label": "Mutan", + "value": "1174" + }, + { + "label": "MagicalBat", + "value": "1188" + }, + { + "label": "Ministryof", + "value": "1212" + }, + { + "label": "MrSly", + "value": "1213" + }, + { + "label": "MsPerfect", + "value": "1214" + }, + { + "label": "MultipleWo", + "value": "1223" + }, + { + "label": "Mukbang", + "value": "1240" + }, + { + "label": "MonsterTar", + "value": "1276" + }, + { + "label": "Mimicry", + "value": "1312" + }, + { + "label": "Multipleid", + "value": "1335" + }, + { + "label": "Matchmadei", + "value": "1347" + }, + { + "label": "Morallessp", + "value": "1350" + }, + { + "label": "MemoryReve", + "value": "1352" + }, + { + "label": "MarriedCou", + "value": "1367" + }, + { + "label": "MindBreak", + "value": "1403" + }, + { + "label": "MaleProtag", + "value": "1507" + }, + { + "label": "MovieDirec", + "value": "1517" + }, + { + "label": "Modernhist", + "value": "1543" + }, + { + "label": "Moneylaund", + "value": "1544" + }, + { + "label": "Misunderst", + "value": "1604" + }, + { + "label": "Massive", + "value": "1606" + }, + { + "label": "Mysterious", + "value": "1621" + }, + { + "label": "MedicalKno", + "value": "1622" + }, + { + "label": "marvelworl", + "value": "1631" + }, + { + "label": "math", + "value": "1633" + }, + { + "label": "Moonwing", + "value": "1640" + }, + { + "label": "MentalIlln", + "value": "1665" + }, + { + "label": "Mojunsheep", + "value": "1677" + }, + { + "label": "Mountainsa", + "value": "1709" + }, + { + "label": "MaskedAce", + "value": "1715" + }, + { + "label": "musicwilll", + "value": "1767" + }, + { + "label": "Moonsea", + "value": "1774" + }, + { + "label": "moonbug", + "value": "1783" + }, + { + "label": "Mingjiao", + "value": "1797" + }, + { + "label": "MarquisofB", + "value": "1805" + }, + { + "label": "Mr.EasyPro", + "value": "1831" + }, + { + "label": "MingjiaoTi", + "value": "1879" + }, + { + "label": "MoXueqing", + "value": "1882" + }, + { + "label": "慕阳", + "value": "1907" + }, + { + "label": "MynameisDa", + "value": "1929" + }, + { + "label": "meowmeow", + "value": "1932" + }, + { + "label": "movingbean", + "value": "1987" + }, + { + "label": "Mountainsa", + "value": "2002" + }, + { + "label": "meetthebea", + "value": "2013" + }, + { + "label": "Mistresspl", + "value": "2015" + }, + { + "label": "man", + "value": "2048" + }, + { + "label": "MistyFlyin", + "value": "2057" + }, + { + "label": "mustdo", + "value": "2060" + }, + { + "label": "mudbodhisa", + "value": "2078" + }, + { + "label": "mylittlesi", + "value": "2079" + }, + { + "label": "moreandmor", + "value": "2080" + }, + { + "label": "Masquerade", + "value": "2103" + }, + { + "label": "makeamirac", + "value": "2109" + }, + { + "label": "Miluo", + "value": "2124" + }, + { + "label": "mynameista", + "value": "2127" + }, + { + "label": "Masterball", + "value": "2134" + }, + { + "label": "milkgrandm", + "value": "2154" + }, + { + "label": "MojiaHills", + "value": "2214" + }, + { + "label": "millionord", + "value": "2231" + }, + { + "label": "MagicTides", + "value": "2244" + }, + { + "label": "MojiaAeros", + "value": "2253" + }, + { + "label": "mythunpara", + "value": "2264" + }, + { + "label": "MangoKK", + "value": "2293" + }, + { + "label": "mythicalma", + "value": "2296" + }, + { + "label": "mywife", + "value": "2303" + }, + { + "label": "Moonlikeah", + "value": "2325" + }, + { + "label": "maninnarut", + "value": "2337" + }, + { + "label": "mapleleafb", + "value": "2354" + }, + { + "label": "MarvelPudd", + "value": "2416" + }, + { + "label": "Mr.Huo", + "value": "2421" + }, + { + "label": "Moyangison", + "value": "2447" + }, + { + "label": "mythicalfi", + "value": "2454" + }, + { + "label": "MagicOne", + "value": "2478" + }, + { + "label": "mixedintwo", + "value": "2481" + }, + { + "label": "Mofamily", + "value": "2498" + }, + { + "label": "MyLubanThi", + "value": "2503" + }, + { + "label": "Makeafortu", + "value": "2517" + }, + { + "label": "MoonlightS", + "value": "2535" + }, + { + "label": "MaskedArmo", + "value": "2548" + }, + { + "label": "medicineme", + "value": "2571" + }, + { + "label": "mambafight", + "value": "2597" + }, + { + "label": "mywifeisya", + "value": "2635" + }, + { + "label": "Moedye", + "value": "2672" + }, + { + "label": "MasterofSi", + "value": "2688" + }, + { + "label": "萌图", + "value": "2700" + }, + { + "label": "Milkgather", + "value": "2713" + }, + { + "label": "mindreadin", + "value": "2730" + }, + { + "label": "Malemainch", + "value": "2736" + }, + { + "label": "Merchant", + "value": "2744" + }, + { + "label": "MoeShinkaw", + "value": "2788" + }, + { + "label": "Motherland", + "value": "2792" + }, + { + "label": "Manhua", + "value": "2865" + }, + { + "label": "MxM", + "value": "2887" + }, + { + "label": "machine", + "value": "2922" + }, + { + "label": "medicalfem", + "value": "2950" + }, + { + "label": "Miracledoc", + "value": "2956" + }, + { + "label": "Motivation", + "value": "2957" + }, + { + "label": "Mechs", + "value": "2962" + }, + { + "label": "MechDesign", + "value": "2963" + }, + { + "label": "Moe", + "value": "3003" + }, + { + "label": "MultipleBo", + "value": "3062" + }, + { + "label": "Merging", + "value": "3071" + }, + { + "label": "Multiplele", + "value": "3085" + }, + { + "label": "MagicDrago", + "value": "3091" + }, + { + "label": "Monstergro", + "value": "3108" + }, + { + "label": "Multiworld", + "value": "3113" + }, + { + "label": "Mostertami", + "value": "3133" + }, + { + "label": "Mag", + "value": "3157" + }, + { + "label": "MonsterCat", + "value": "3159" + }, + { + "label": "Mutatedbea", + "value": "3181" + }, + { + "label": "Milf", + "value": "3202" + }, + { + "label": "Martialart", + "value": "3235" + }, + { + "label": "Naruto", + "value": "116" + }, + { + "label": "Nationalis", + "value": "110" + }, + { + "label": "Nobles", + "value": "172" + }, + { + "label": "NaiveProta", + "value": "82" + }, + { + "label": "NoRomance", + "value": "672" + }, + { + "label": "NoCp", + "value": "1208" + }, + { + "label": "Ninjas", + "value": "247" + }, + { + "label": "Necromance", + "value": "574" + }, + { + "label": "NonHuman", + "value": "1623" + }, + { + "label": "Non-System", + "value": "654" + }, + { + "label": "Netori", + "value": "84" + }, + { + "label": "Near-Death", + "value": "83" + }, + { + "label": "NA", + "value": "613" + }, + { + "label": "No-Harem", + "value": "3079" + }, + { + "label": "NBA", + "value": "459" + }, + { + "label": "NoHarem", + "value": "832" + }, + { + "label": "Netorare", + "value": "360" + }, + { + "label": "Npc", + "value": "3165" + }, + { + "label": "Nocheats", + "value": "3172" + }, + { + "label": "Non-humanP", + "value": "1382" + }, + { + "label": "Nightmare", + "value": "125" + }, + { + "label": "Nurses", + "value": "440" + }, + { + "label": "Nightmares", + "value": "890" + }, + { + "label": "Necromancy", + "value": "2939" + }, + { + "label": "NoSystem", + "value": "1131" + }, + { + "label": "NonHumanPr", + "value": "1132" + }, + { + "label": "Navy", + "value": "1359" + }, + { + "label": "Nine-Taile", + "value": "2038" + }, + { + "label": "Noble", + "value": "1203" + }, + { + "label": "nightsilen", + "value": "1738" + }, + { + "label": "NangongHan", + "value": "1975" + }, + { + "label": "Non-humano", + "value": "281" + }, + { + "label": "Narcissist", + "value": "565" + }, + { + "label": "NoPairing", + "value": "803" + }, + { + "label": "Napoleon", + "value": "1081" + }, + { + "label": "ninja", + "value": "1375" + }, + { + "label": "Neet", + "value": "1433" + }, + { + "label": "Nerd", + "value": "1518" + }, + { + "label": "Nobility", + "value": "1537" + }, + { + "label": "NationBuil", + "value": "1626" + }, + { + "label": "饕牛", + "value": "1682" + }, + { + "label": "nightfire", + "value": "1714" + }, + { + "label": "Ninjapirat", + "value": "1718" + }, + { + "label": "NinefoldSe", + "value": "1809" + }, + { + "label": "NineWarsof", + "value": "1931" + }, + { + "label": "Nosnacks", + "value": "1933" + }, + { + "label": "NiuBao", + "value": "1936" + }, + { + "label": "Nanshen", + "value": "2163" + }, + { + "label": "NiangkouSa", + "value": "2202" + }, + { + "label": "Nine-color", + "value": "2288" + }, + { + "label": "NarutoClou", + "value": "2330" + }, + { + "label": "narutoceda", + "value": "2334" + }, + { + "label": "NarutoQuiz", + "value": "2340" + }, + { + "label": "notscary", + "value": "2342" + }, + { + "label": "notlevelth", + "value": "2439" + }, + { + "label": "neverfail", + "value": "2442" + }, + { + "label": "NarutoxRea", + "value": "2460" + }, + { + "label": "noncat", + "value": "2461" + }, + { + "label": "Nidouzi", + "value": "2462" + }, + { + "label": "Nowadays", + "value": "2482" + }, + { + "label": "Noless", + "value": "2576" + }, + { + "label": "nightdance", + "value": "2602" + }, + { + "label": "NoGoldFing", + "value": "2918" + }, + { + "label": "NoblesPoli", + "value": "2933" + }, + { + "label": "NTL", + "value": "2936" + }, + { + "label": "Non-HumanM", + "value": "2937" + }, + { + "label": "no-misunde", + "value": "3137" + }, + { + "label": "NSFW", + "value": "3200" + }, + { + "label": "Novel", + "value": "3238" + }, + { + "label": "Non-linear", + "value": "3249" + }, + { + "label": "OnePiece", + "value": "117" + }, + { + "label": "Obsession", + "value": "2851" + }, + { + "label": "Overpowere", + "value": "2955" + }, + { + "label": "OuterSpace", + "value": "220" + }, + { + "label": "Omegaverse", + "value": "14" + }, + { + "label": "OlderLoveI", + "value": "177" + }, + { + "label": "OPMC", + "value": "862" + }, + { + "label": "Orphans", + "value": "358" + }, + { + "label": "Otaku", + "value": "478" + }, + { + "label": "ObsessiveL", + "value": "468" + }, + { + "label": "Orcs", + "value": "537" + }, + { + "label": "OrganizedC", + "value": "529" + }, + { + "label": "OnlineRoma", + "value": "536" + }, + { + "label": "OfficeRoma", + "value": "578" + }, + { + "label": "Overpowere", + "value": "1058" + }, + { + "label": "OPProtagon", + "value": "787" + }, + { + "label": "OnlineGame", + "value": "936" + }, + { + "label": "Ordinary", + "value": "3132" + }, + { + "label": "Orphan", + "value": "777" + }, + { + "label": "Organizati", + "value": "1114" + }, + { + "label": "onlyloveyo", + "value": "1804" + }, + { + "label": "Original", + "value": "3059" + }, + { + "label": "Onlinegami", + "value": "3070" + }, + { + "label": "overpower", + "value": "3229" + }, + { + "label": "Overpowere", + "value": "25" + }, + { + "label": "OnePunchMa", + "value": "680" + }, + { + "label": "Orc", + "value": "840" + }, + { + "label": "Operation", + "value": "2754" + }, + { + "label": "Onenightst", + "value": "3046" + }, + { + "label": "Overprotec", + "value": "697" + }, + { + "label": "OPheroine", + "value": "1039" + }, + { + "label": "On-HookSys", + "value": "1220" + }, + { + "label": "Orientalfa", + "value": "1448" + }, + { + "label": "Otherworld", + "value": "1530" + }, + { + "label": "Orphaned", + "value": "1535" + }, + { + "label": "Officialdo", + "value": "1545" + }, + { + "label": "OrphanMC", + "value": "1570" + }, + { + "label": "Orcsworld", + "value": "1589" + }, + { + "label": "Onepunch", + "value": "1596" + }, + { + "label": "openasmall", + "value": "1659" + }, + { + "label": "oldage", + "value": "1761" + }, + { + "label": "OldQinpeop", + "value": "1853" + }, + { + "label": "OneSwordFl", + "value": "1890" + }, + { + "label": "oldfisheat", + "value": "1899" + }, + { + "label": "Onethousan", + "value": "1935" + }, + { + "label": "Otezetta", + "value": "1971" + }, + { + "label": "olddemon", + "value": "1983" + }, + { + "label": "Obanbrothe", + "value": "2009" + }, + { + "label": "orangeappl", + "value": "2104" + }, + { + "label": "onparadise", + "value": "2142" + }, + { + "label": "OriginalUn", + "value": "2178" + }, + { + "label": "Openyourey", + "value": "2191" + }, + { + "label": "oooobe", + "value": "2251" + }, + { + "label": "ohmygod", + "value": "2307" + }, + { + "label": "Oldghostsm", + "value": "2319" + }, + { + "label": "OTTGroupCh", + "value": "2332" + }, + { + "label": "oldtombrob", + "value": "2343" + }, + { + "label": "Onepunchmo", + "value": "2370" + }, + { + "label": "OneLeafRed", + "value": "2375" + }, + { + "label": "oldfaceunc", + "value": "2579" + }, + { + "label": "OriginalYe", + "value": "2587" + }, + { + "label": "Oneflower", + "value": "2593" + }, + { + "label": "onetree", + "value": "2594" + }, + { + "label": "onemelonri", + "value": "2595" + }, + { + "label": "Oneyearold", + "value": "2603" + }, + { + "label": "onlyyouth", + "value": "2609" + }, + { + "label": "Onepunchto", + "value": "2616" + }, + { + "label": "Origuchi", + "value": "2623" + }, + { + "label": "onemeterst", + "value": "2675" + }, + { + "label": "One-Piece", + "value": "2732" + }, + { + "label": "offical", + "value": "2756" + }, + { + "label": "OverheadHi", + "value": "2764" + }, + { + "label": "Official", + "value": "2874" + }, + { + "label": "Olderlovei", + "value": "2900" + }, + { + "label": "Over-Power", + "value": "2902" + }, + { + "label": "omega", + "value": "2911" + }, + { + "label": "OpFemalePr", + "value": "2914" + }, + { + "label": "Overlord", + "value": "2997" + }, + { + "label": "Otherworld", + "value": "3099" + }, + { + "label": "Orientalmy", + "value": "3110" + }, + { + "label": "Overpowere", + "value": "3185" + }, + { + "label": "Outdoors", + "value": "3205" + }, + { + "label": "Officialwo", + "value": "3237" + }, + { + "label": "Onlinegame", + "value": "3240" + }, + { + "label": "PoortoRich", + "value": "27" + }, + { + "label": "Politics", + "value": "289" + }, + { + "label": "Pets", + "value": "105" + }, + { + "label": "Possession", + "value": "410" + }, + { + "label": "Polygamy", + "value": "174" + }, + { + "label": "Pregnancy", + "value": "384" + }, + { + "label": "Possessive", + "value": "4" + }, + { + "label": "Post-apoca", + "value": "217" + }, + { + "label": "Pokemon", + "value": "555" + }, + { + "label": "PowerCoupl", + "value": "193" + }, + { + "label": "ParallelWo", + "value": "252" + }, + { + "label": "PureLove", + "value": "2858" + }, + { + "label": "Pirates", + "value": "390" + }, + { + "label": "Police", + "value": "558" + }, + { + "label": "PastPlaysa", + "value": "371" + }, + { + "label": "PreviousLi", + "value": "224" + }, + { + "label": "Powerfulco", + "value": "945" + }, + { + "label": "PoorProtag", + "value": "451" + }, + { + "label": "PastTrauma", + "value": "583" + }, + { + "label": "Possessive", + "value": "2888" + }, + { + "label": "PervertedP", + "value": "173" + }, + { + "label": "PopularLov", + "value": "547" + }, + { + "label": "PillConcot", + "value": "626" + }, + { + "label": "PillConcoc", + "value": "235" + }, + { + "label": "PrinceofTe", + "value": "649" + }, + { + "label": "Poisons", + "value": "447" + }, + { + "label": "PowerStrug", + "value": "587" + }, + { + "label": "PragmaticP", + "value": "428" + }, + { + "label": "Psychologi", + "value": "1611" + }, + { + "label": "PlayfulPro", + "value": "589" + }, + { + "label": "ParallelWo", + "value": "852" + }, + { + "label": "Polyandry", + "value": "530" + }, + { + "label": "ProactiveP", + "value": "854" + }, + { + "label": "Psychopath", + "value": "480" + }, + { + "label": "Prison", + "value": "584" + }, + { + "label": "Parody", + "value": "919" + }, + { + "label": "Princess", + "value": "1119" + }, + { + "label": "Positive", + "value": "2968" + }, + { + "label": "PillBasedC", + "value": "26" + }, + { + "label": "PsychicPow", + "value": "918" + }, + { + "label": "Pharmacist", + "value": "501" + }, + { + "label": "Personalit", + "value": "1327" + }, + { + "label": "ParentComp", + "value": "446" + }, + { + "label": "Prostitute", + "value": "1409" + }, + { + "label": "PreviousLi", + "value": "325" + }, + { + "label": "Pet", + "value": "789" + }, + { + "label": "Priests", + "value": "794" + }, + { + "label": "PretendLov", + "value": "1197" + }, + { + "label": "Pilots", + "value": "1331" + }, + { + "label": "Playboys", + "value": "1435" + }, + { + "label": "Programmer", + "value": "208" + }, + { + "label": "Poetry", + "value": "609" + }, + { + "label": "PoliteProt", + "value": "1415" + }, + { + "label": "Prophecies", + "value": "525" + }, + { + "label": "PortableSp", + "value": "930" + }, + { + "label": "Protagonis", + "value": "1245" + }, + { + "label": "Precogniti", + "value": "1427" + }, + { + "label": "Persistent", + "value": "271" + }, + { + "label": "PamperingR", + "value": "377" + }, + { + "label": "PerfectWor", + "value": "684" + }, + { + "label": "Possessive", + "value": "716" + }, + { + "label": "Paranoid", + "value": "887" + }, + { + "label": "Primitivew", + "value": "1061" + }, + { + "label": "President", + "value": "1316" + }, + { + "label": "Pirate", + "value": "1376" + }, + { + "label": "Phoenixes", + "value": "1420" + }, + { + "label": "Paizuri", + "value": "1429" + }, + { + "label": "Part-TimeJ", + "value": "1434" + }, + { + "label": "PrinceQing", + "value": "2502" + }, + { + "label": "Parasites", + "value": "43" + }, + { + "label": "Protagonis", + "value": "175" + }, + { + "label": "Protagonis", + "value": "318" + }, + { + "label": "Protagonis", + "value": "403" + }, + { + "label": "Philosophi", + "value": "608" + }, + { + "label": "poems", + "value": "633" + }, + { + "label": "Protagonis", + "value": "644" + }, + { + "label": "Protagonis", + "value": "694" + }, + { + "label": "Painting", + "value": "741" + }, + { + "label": "Players", + "value": "767" + }, + { + "label": "PoliticalS", + "value": "804" + }, + { + "label": "PovertyAll", + "value": "805" + }, + { + "label": "PseudoHolo", + "value": "806" + }, + { + "label": "PseudoReli", + "value": "807" + }, + { + "label": "Playboymal", + "value": "814" + }, + { + "label": "PastPlaysa", + "value": "932" + }, + { + "label": "Prehistori", + "value": "942" + }, + { + "label": "Prehistori", + "value": "959" + }, + { + "label": "Partnerofa", + "value": "964" + }, + { + "label": "PositiveLe", + "value": "1019" + }, + { + "label": "PlayingGho", + "value": "1062" + }, + { + "label": "Protagonis", + "value": "1117" + }, + { + "label": "Puzzles", + "value": "1122" + }, + { + "label": "PoorRoRich", + "value": "1129" + }, + { + "label": "Psychology", + "value": "1140" + }, + { + "label": "Parasite", + "value": "1153" + }, + { + "label": "Player", + "value": "1162" + }, + { + "label": "PlayerKill", + "value": "1277" + }, + { + "label": "PresentDay", + "value": "1340" + }, + { + "label": "Photograph", + "value": "1449" + }, + { + "label": "ParalelWor", + "value": "1479" + }, + { + "label": "Priestesse", + "value": "1503" + }, + { + "label": "Pills", + "value": "1520" + }, + { + "label": "Professor", + "value": "1571" + }, + { + "label": "PoliticalI", + "value": "1574" + }, + { + "label": "Patriarch", + "value": "1593" + }, + { + "label": "PowersTran", + "value": "1600" + }, + { + "label": "Psychic", + "value": "1601" + }, + { + "label": "PoisonMout", + "value": "1605" + }, + { + "label": "Poorcrazy", + "value": "1637" + }, + { + "label": "PeerlessSw", + "value": "1643" + }, + { + "label": "Peerlessso", + "value": "1647" + }, + { + "label": "perfectmag", + "value": "1651" + }, + { + "label": "PirateDaQi", + "value": "1702" + }, + { + "label": "Piratehaha", + "value": "1766" + }, + { + "label": "Punch", + "value": "1814" + }, + { + "label": "part-timeo", + "value": "1875" + }, + { + "label": "pleasantin", + "value": "1896" + }, + { + "label": "PlayBlueMo", + "value": "1919" + }, + { + "label": "pendreamst", + "value": "1947" + }, + { + "label": "Positiveel", + "value": "1954" + }, + { + "label": "plumthirte", + "value": "2005" + }, + { + "label": "PirateGrea", + "value": "2010" + }, + { + "label": "PokémonVo", + "value": "2014" + }, + { + "label": "panic", + "value": "2026" + }, + { + "label": "Pipifish", + "value": "2088" + }, + { + "label": "paleandwhi", + "value": "2118" + }, + { + "label": "purekitten", + "value": "2339" + }, + { + "label": "Pirateacto", + "value": "2346" + }, + { + "label": "PirateWars", + "value": "2367" + }, + { + "label": "PokémonTi", + "value": "2369" + }, + { + "label": "PopeBibiDo", + "value": "2371" + }, + { + "label": "PirateCour", + "value": "2372" + }, + { + "label": "petsurviva", + "value": "2389" + }, + { + "label": "pureimpuls", + "value": "2414" + }, + { + "label": "PiratexFai", + "value": "2434" + }, + { + "label": "pigeonnext", + "value": "2469" + }, + { + "label": "Peoplenear", + "value": "2510" + }, + { + "label": "Papaisvery", + "value": "2521" + }, + { + "label": "Piscesinth", + "value": "2566" + }, + { + "label": "potatogirl", + "value": "2606" + }, + { + "label": "PiratesofH", + "value": "2629" + }, + { + "label": "PokémonGo", + "value": "2640" + }, + { + "label": "pendragon", + "value": "2708" + }, + { + "label": "PrinceofHe", + "value": "2729" + }, + { + "label": "Protagonis", + "value": "2739" + }, + { + "label": "psionic", + "value": "2774" + }, + { + "label": "Pleaseforg", + "value": "2785" + }, + { + "label": "Peasant", + "value": "2797" + }, + { + "label": "PhantomThi", + "value": "2801" + }, + { + "label": "Photograph", + "value": "2828" + }, + { + "label": "Programmin", + "value": "2869" + }, + { + "label": "PlaneWars", + "value": "2879" + }, + { + "label": "PrimitiveT", + "value": "2906" + }, + { + "label": "Poor", + "value": "2930" + }, + { + "label": "Prince", + "value": "2935" + }, + { + "label": "palace", + "value": "2948" + }, + { + "label": "Preview", + "value": "3005" + }, + { + "label": "Popular", + "value": "3017" + }, + { + "label": "PrettyGirl", + "value": "3018" + }, + { + "label": "Pamper", + "value": "3041" + }, + { + "label": "Princeandp", + "value": "3065" + }, + { + "label": "Petbeasts", + "value": "3080" + }, + { + "label": "Putin", + "value": "3086" + }, + { + "label": "Parallelsp", + "value": "3120" + }, + { + "label": "Passiveski", + "value": "3136" + }, + { + "label": "Payback", + "value": "3166" + }, + { + "label": "QuickTrans", + "value": "449" + }, + { + "label": "qidian", + "value": "2943" + }, + { + "label": "Quickwear", + "value": "1143" + }, + { + "label": "QuirkyChar", + "value": "1402" + }, + { + "label": "QiLuck", + "value": "1244" + }, + { + "label": "QuietChara", + "value": "1372" + }, + { + "label": "qimao", + "value": "3214" + }, + { + "label": "QuickPass", + "value": "1227" + }, + { + "label": "QuickTrans", + "value": "1259" + }, + { + "label": "Question&a", + "value": "1334" + }, + { + "label": "QT", + "value": "1379" + }, + { + "label": "QuickTrans", + "value": "1602" + }, + { + "label": "quietflowe", + "value": "1698" + }, + { + "label": "Qingfeng1D", + "value": "1701" + }, + { + "label": "QinBichu", + "value": "1706" + }, + { + "label": "Quasi-GodS", + "value": "1768" + }, + { + "label": "Qingliansw", + "value": "1819" + }, + { + "label": "QingheTaoi", + "value": "1885" + }, + { + "label": "quitesharp", + "value": "1952" + }, + { + "label": "QiXuan", + "value": "1969" + }, + { + "label": "qingyu", + "value": "2215" + }, + { + "label": "QueenofBla", + "value": "2302" + }, + { + "label": "Quququ", + "value": "2496" + }, + { + "label": "QianshanTw", + "value": "2600" + }, + { + "label": "清裁", + "value": "2722" + }, + { + "label": "Reincarnat", + "value": "95" + }, + { + "label": "Romance", + "value": "689" + }, + { + "label": "R18", + "value": "1095" + }, + { + "label": "Rebirth", + "value": "28" + }, + { + "label": "Revenge", + "value": "126" + }, + { + "label": "RomanticSu", + "value": "225" + }, + { + "label": "Racism", + "value": "111" + }, + { + "label": "RuthlessPr", + "value": "203" + }, + { + "label": "Royalty", + "value": "15" + }, + { + "label": "RebirthedP", + "value": "643" + }, + { + "label": "Rape", + "value": "236" + }, + { + "label": "Regret", + "value": "2853" + }, + { + "label": "Regression", + "value": "2856" + }, + { + "label": "R-18", + "value": "553" + }, + { + "label": "ReverseHar", + "value": "212" + }, + { + "label": "R-15", + "value": "617" + }, + { + "label": "Religions", + "value": "591" + }, + { + "label": "Resurrecti", + "value": "222" + }, + { + "label": "Rarebloodl", + "value": "2988" + }, + { + "label": "Royalfamil", + "value": "2995" + }, + { + "label": "Rpe", + "value": "5" + }, + { + "label": "ReverseRap", + "value": "1412" + }, + { + "label": "RighteousP", + "value": "778" + }, + { + "label": "RomanceFan", + "value": "2854" + }, + { + "label": "Reborn", + "value": "899" + }, + { + "label": "RaceChange", + "value": "407" + }, + { + "label": "Rivalry", + "value": "884" + }, + { + "label": "RichProtag", + "value": "875" + }, + { + "label": "Restaurant", + "value": "896" + }, + { + "label": "Roommates", + "value": "996" + }, + { + "label": "Richfamily", + "value": "999" + }, + { + "label": "Reikyrecov", + "value": "713" + }, + { + "label": "Rich", + "value": "827" + }, + { + "label": "RichtoPoor", + "value": "889" + }, + { + "label": "RuthlessMc", + "value": "1033" + }, + { + "label": "Reporters", + "value": "1462" + }, + { + "label": "Righteous", + "value": "2973" + }, + { + "label": "Richdaught", + "value": "2979" + }, + { + "label": "Reversal", + "value": "3215" + }, + { + "label": "RapeVictim", + "value": "6" + }, + { + "label": "Reunion", + "value": "1067" + }, + { + "label": "RimuruTemp", + "value": "1193" + }, + { + "label": "relaxed", + "value": "1384" + }, + { + "label": "Rebellion", + "value": "1432" + }, + { + "label": "Rideawhale", + "value": "1670" + }, + { + "label": "riversande", + "value": "1862" + }, + { + "label": "Redemption", + "value": "2859" + }, + { + "label": "RPG", + "value": "2892" + }, + { + "label": "RPGSystem", + "value": "3179" + }, + { + "label": "Reincarnat", + "value": "354" + }, + { + "label": "Returningf", + "value": "355" + }, + { + "label": "Reincarnat", + "value": "408" + }, + { + "label": "Reincarnat", + "value": "502" + }, + { + "label": "Rune", + "value": "754" + }, + { + "label": "RomanticSu", + "value": "868" + }, + { + "label": "RookieProt", + "value": "951" + }, + { + "label": "Reincarnat", + "value": "1007" + }, + { + "label": "robots", + "value": "1044" + }, + { + "label": "Regressor", + "value": "1163" + }, + { + "label": "Ras", + "value": "1274" + }, + { + "label": "ReverseRpe", + "value": "1286" + }, + { + "label": "Ruthless", + "value": "1298" + }, + { + "label": "Researcher", + "value": "1302" + }, + { + "label": "reincarnat", + "value": "1348" + }, + { + "label": "RpeVictimB", + "value": "1354" + }, + { + "label": "RankingLis", + "value": "1356" + }, + { + "label": "ReikiRecov", + "value": "1389" + }, + { + "label": "Russian", + "value": "1390" + }, + { + "label": "Reversible", + "value": "1436" + }, + { + "label": "Reincarnat", + "value": "1452" + }, + { + "label": "RedAlert2", + "value": "1474" + }, + { + "label": "RapeVictim", + "value": "1481" + }, + { + "label": "RomanticPr", + "value": "1486" + }, + { + "label": "Russia", + "value": "1546" + }, + { + "label": "Redalert", + "value": "1552" + }, + { + "label": "Role-Playi", + "value": "1558" + }, + { + "label": "Reincarnat", + "value": "1563" + }, + { + "label": "ReligousOr", + "value": "1572" + }, + { + "label": "ReligiousO", + "value": "1599" + }, + { + "label": "RinYueqing", + "value": "1644" + }, + { + "label": "runawaycit", + "value": "1663" + }, + { + "label": "runawayant", + "value": "1692" + }, + { + "label": "RoyalSabur", + "value": "1807" + }, + { + "label": "reversesmo", + "value": "1903" + }, + { + "label": "Rapeseedra", + "value": "2016" + }, + { + "label": "Ruoshuithr", + "value": "2096" + }, + { + "label": "rainandsno", + "value": "2111" + }, + { + "label": "reallyking", + "value": "2137" + }, + { + "label": "restaurant", + "value": "2143" + }, + { + "label": "recreation", + "value": "2180" + }, + { + "label": "Residencen", + "value": "2256" + }, + { + "label": "richeveryy", + "value": "2388" + }, + { + "label": "RabbitToot", + "value": "2411" + }, + { + "label": "Roon", + "value": "2431" + }, + { + "label": "raisedache", + "value": "2473" + }, + { + "label": "Raiseaghos", + "value": "2486" + }, + { + "label": "Rotaryhotp", + "value": "2513" + }, + { + "label": "rainboweig", + "value": "2558" + }, + { + "label": "Resurrecti", + "value": "2580" + }, + { + "label": "RedDustDru", + "value": "2617" + }, + { + "label": "rainydaywi", + "value": "2668" + }, + { + "label": "Realmmonst", + "value": "2692" + }, + { + "label": "Residentev", + "value": "2745" + }, + { + "label": "RiseofGras", + "value": "2819" + }, + { + "label": "Reiki", + "value": "2870" + }, + { + "label": "Reincarnat", + "value": "2932" + }, + { + "label": "Reverse", + "value": "2934" + }, + { + "label": "Romanticlo", + "value": "2975" + }, + { + "label": "Romancecom", + "value": "3048" + }, + { + "label": "Races", + "value": "3096" + }, + { + "label": "Righteousn", + "value": "3134" + }, + { + "label": "Rise", + "value": "3146" + }, + { + "label": "Return", + "value": "3222" + }, + { + "label": "reasoning", + "value": "3224" + }, + { + "label": "RanchFarmi", + "value": "3234" + }, + { + "label": "System", + "value": "204" + }, + { + "label": "SystemAdmi", + "value": "76" + }, + { + "label": "SecondChan", + "value": "30" + }, + { + "label": "SpecialAbi", + "value": "183" + }, + { + "label": "Showbiz", + "value": "58" + }, + { + "label": "Superpower", + "value": "463" + }, + { + "label": "SliceofLif", + "value": "765" + }, + { + "label": "Survival", + "value": "112" + }, + { + "label": "SlowRomanc", + "value": "194" + }, + { + "label": "Sign-InChe", + "value": "653" + }, + { + "label": "StrongtoSt", + "value": "264" + }, + { + "label": "SwordAndMa", + "value": "182" + }, + { + "label": "ShamelessP", + "value": "96" + }, + { + "label": "StrongLove", + "value": "214" + }, + { + "label": "Scientists", + "value": "477" + }, + { + "label": "SemeProtag", + "value": "564" + }, + { + "label": "SuperTechn", + "value": "645" + }, + { + "label": "SurvivalGa", + "value": "362" + }, + { + "label": "SummoningM", + "value": "246" + }, + { + "label": "SwordWield", + "value": "99" + }, + { + "label": "SlowGrowth", + "value": "282" + }, + { + "label": "StrongProt", + "value": "657" + }, + { + "label": "Strongfrom", + "value": "197" + }, + { + "label": "SmartCoupl", + "value": "415" + }, + { + "label": "Strategist", + "value": "604" + }, + { + "label": "Slaves", + "value": "181" + }, + { + "label": "Sweetlove", + "value": "2958" + }, + { + "label": "SchoolLife", + "value": "707" + }, + { + "label": "ShoujoAi", + "value": "708" + }, + { + "label": "SecretIden", + "value": "179" + }, + { + "label": "Soccer", + "value": "405" + }, + { + "label": "SuddenWeal", + "value": "458" + }, + { + "label": "StrongBack", + "value": "671" + }, + { + "label": "Supernatur", + "value": "772" + }, + { + "label": "SicklyChar", + "value": "540" + }, + { + "label": "StoreOwner", + "value": "74" + }, + { + "label": "Swordsman", + "value": "622" + }, + { + "label": "Singers", + "value": "253" + }, + { + "label": "Sports", + "value": "674" + }, + { + "label": "Spaceship", + "value": "283" + }, + { + "label": "StrategicB", + "value": "298" + }, + { + "label": "Sci-fi", + "value": "755" + }, + { + "label": "SweetText", + "value": "781" + }, + { + "label": "Smut", + "value": "2984" + }, + { + "label": "SecretOrga", + "value": "596" + }, + { + "label": "SecretOrga", + "value": "992" + }, + { + "label": "Summons", + "value": "1050" + }, + { + "label": "Spirits", + "value": "484" + }, + { + "label": "SpaceOpera", + "value": "619" + }, + { + "label": "Sweet", + "value": "866" + }, + { + "label": "SectDevelo", + "value": "31" + }, + { + "label": "SexualAbus", + "value": "505" + }, + { + "label": "SinglePare", + "value": "594" + }, + { + "label": "SentientSk", + "value": "656" + }, + { + "label": "Soldiers", + "value": "263" + }, + { + "label": "Shoujo-AiS", + "value": "455" + }, + { + "label": "Souls", + "value": "546" + }, + { + "label": "SisterComp", + "value": "356" + }, + { + "label": "SlaveProta", + "value": "570" + }, + { + "label": "Shounen-Ai", + "value": "720" + }, + { + "label": "Sects", + "value": "867" + }, + { + "label": "SuddenStre", + "value": "1086" + }, + { + "label": "Sciencefic", + "value": "1268" + }, + { + "label": "Salvation", + "value": "2867" + }, + { + "label": "SkillAssim", + "value": "385" + }, + { + "label": "Siblings", + "value": "520" + }, + { + "label": "SoulPower", + "value": "566" + }, + { + "label": "SummonedHe", + "value": "397" + }, + { + "label": "Space", + "value": "830" + }, + { + "label": "SkillBooks", + "value": "441" + }, + { + "label": "SerialKill", + "value": "486" + }, + { + "label": "Summoner", + "value": "711" + }, + { + "label": "SecretCrus", + "value": "753" + }, + { + "label": "StrongFema", + "value": "1041" + }, + { + "label": "Shapeshift", + "value": "1619" + }, + { + "label": "SpecialAbi", + "value": "127" + }, + { + "label": "Secrets", + "value": "180" + }, + { + "label": "Saints", + "value": "394" + }, + { + "label": "StraightUk", + "value": "727" + }, + { + "label": "SmartMC", + "value": "1034" + }, + { + "label": "SealedPowe", + "value": "178" + }, + { + "label": "SavingtheW", + "value": "492" + }, + { + "label": "StockholmS", + "value": "506" + }, + { + "label": "SelfishPro", + "value": "538" + }, + { + "label": "smartprota", + "value": "721" + }, + { + "label": "StrongMC", + "value": "746" + }, + { + "label": "Schemesand", + "value": "883" + }, + { + "label": "Shapeshift", + "value": "1416" + }, + { + "label": "Scary", + "value": "3089" + }, + { + "label": "SecretiveP", + "value": "326" + }, + { + "label": "ShortStory", + "value": "352" + }, + { + "label": "SkillCreat", + "value": "386" + }, + { + "label": "StubbornPr", + "value": "479" + }, + { + "label": "ShyCharact", + "value": "599" + }, + { + "label": "SigninChec", + "value": "683" + }, + { + "label": "Superstar", + "value": "1395" + }, + { + "label": "Singlefema", + "value": "2972" + }, + { + "label": "SpiritAdvi", + "value": "98" + }, + { + "label": "SelflessPr", + "value": "351" + }, + { + "label": "SadisticCh", + "value": "456" + }, + { + "label": "SpearWield", + "value": "585" + }, + { + "label": "summon", + "value": "611" + }, + { + "label": "Superpower", + "value": "791" + }, + { + "label": "Suicides", + "value": "823" + }, + { + "label": "Sign-in", + "value": "826" + }, + { + "label": "StrongCoup", + "value": "984" + }, + { + "label": "Simulator", + "value": "1008" + }, + { + "label": "StoicChara", + "value": "1078" + }, + { + "label": "Seduction", + "value": "1383" + }, + { + "label": "Servants", + "value": "1418" + }, + { + "label": "SexSlaves", + "value": "1465" + }, + { + "label": "straightma", + "value": "2752" + }, + { + "label": "signin", + "value": "997" + }, + { + "label": "Slice-of-l", + "value": "1020" + }, + { + "label": "Slave", + "value": "1028" + }, + { + "label": "Shounen", + "value": "1127" + }, + { + "label": "SingleHero", + "value": "1169" + }, + { + "label": "Succubus", + "value": "1200" + }, + { + "label": "secretary", + "value": "1344" + }, + { + "label": "Spies", + "value": "1421" + }, + { + "label": "Summoning", + "value": "1613" + }, + { + "label": "Strategy", + "value": "2820" + }, + { + "label": "Spoiling", + "value": "2991" + }, + { + "label": "Scifi", + "value": "3068" + }, + { + "label": "Shota", + "value": "270" + }, + { + "label": "SpiritUser", + "value": "483" + }, + { + "label": "science", + "value": "634" + }, + { + "label": "SpecialFor", + "value": "679" + }, + { + "label": "SwallowedS", + "value": "688" + }, + { + "label": "SentientOb", + "value": "703" + }, + { + "label": "Suspense", + "value": "704" + }, + { + "label": "StrongLove", + "value": "751" + }, + { + "label": "SCP", + "value": "792" + }, + { + "label": "Star", + "value": "846" + }, + { + "label": "Sentimenta", + "value": "849" + }, + { + "label": "SexualCult", + "value": "914" + }, + { + "label": "Saint", + "value": "924" + }, + { + "label": "ShouProtag", + "value": "1036" + }, + { + "label": "SlowLife", + "value": "1059" + }, + { + "label": "SpecialLik", + "value": "1068" + }, + { + "label": "Sea", + "value": "1170" + }, + { + "label": "Simulation", + "value": "1251" + }, + { + "label": "Shuangwen", + "value": "1261" + }, + { + "label": "SecondChan", + "value": "1267" + }, + { + "label": "shounenai", + "value": "1321" + }, + { + "label": "SaikiK", + "value": "1336" + }, + { + "label": "StraightSe", + "value": "1404" + }, + { + "label": "SpatialMan", + "value": "1444" + }, + { + "label": "SlaveHarem", + "value": "1463" + }, + { + "label": "SaintSeiya", + "value": "1608" + }, + { + "label": "smallninel", + "value": "1746" + }, + { + "label": "Swordgod", + "value": "2457" + }, + { + "label": "stallion", + "value": "2882" + }, + { + "label": "Self-disci", + "value": "2898" + }, + { + "label": "Strongfema", + "value": "3026" + }, + { + "label": "School", + "value": "3081" + }, + { + "label": "Serious", + "value": "3092" + }, + { + "label": "Seductive", + "value": "3144" + }, + { + "label": "SchemesAnd", + "value": "29" + }, + { + "label": "Student-Te", + "value": "184" + }, + { + "label": "Strength-b", + "value": "213" + }, + { + "label": "SiblingsNo", + "value": "272" + }, + { + "label": "SecondChan", + "value": "636" + }, + { + "label": "Scavengers", + "value": "637" + }, + { + "label": "SpiritualQ", + "value": "651" + }, + { + "label": "SwordArtOn", + "value": "666" + }, + { + "label": "SystemTran", + "value": "682" + }, + { + "label": "SeeingThin", + "value": "702" + }, + { + "label": "StrongFema", + "value": "717" + }, + { + "label": "SpiritualR", + "value": "732" + }, + { + "label": "systemowne", + "value": "768" + }, + { + "label": "SpiritAnal", + "value": "775" + }, + { + "label": "SchemingPr", + "value": "808" + }, + { + "label": "SpecialLov", + "value": "869" + }, + { + "label": "SameSexMar", + "value": "886" + }, + { + "label": "Sequel", + "value": "901" + }, + { + "label": "SkillSteal", + "value": "943" + }, + { + "label": "SaikiK.", + "value": "986" + }, + { + "label": "Schemes", + "value": "1025" + }, + { + "label": "Son-in-law", + "value": "1026" + }, + { + "label": "SystemTran", + "value": "1037" + }, + { + "label": "sweetroman", + "value": "1069" + }, + { + "label": "Sysetm", + "value": "1088" + }, + { + "label": "Shelter", + "value": "1094" + }, + { + "label": "StrongestP", + "value": "1108" + }, + { + "label": "Scientist", + "value": "1115" + }, + { + "label": "Supportive", + "value": "1116" + }, + { + "label": "Superheroe", + "value": "1145" + }, + { + "label": "SpecialAbi", + "value": "1158" + }, + { + "label": "SlapstickC", + "value": "1166" + }, + { + "label": "SecretRela", + "value": "1228" + }, + { + "label": "Stepmother", + "value": "1233" + }, + { + "label": "SystemFlow", + "value": "1248" + }, + { + "label": "SchoolSett", + "value": "1273" + }, + { + "label": "Siscon", + "value": "1332" + }, + { + "label": "Sailing", + "value": "1345" + }, + { + "label": "schemeandc", + "value": "1355" + }, + { + "label": "strongfema", + "value": "1362" + }, + { + "label": "submissive", + "value": "1381" + }, + { + "label": "singer", + "value": "1396" + }, + { + "label": "suicidalpr", + "value": "1401" + }, + { + "label": "Shotacon", + "value": "1425" + }, + { + "label": "Sibling&am", + "value": "1426" + }, + { + "label": "SevenDeadl", + "value": "1440" + }, + { + "label": "Sharp-tong", + "value": "1450" + }, + { + "label": "SocialOutc", + "value": "1454" + }, + { + "label": "SiblingRiv", + "value": "1457" + }, + { + "label": "SxSlaves", + "value": "1478" + }, + { + "label": "SlaveSyste", + "value": "1482" + }, + { + "label": "StrongSubo", + "value": "1484" + }, + { + "label": "SuperSemin", + "value": "1485" + }, + { + "label": "Strongsubo", + "value": "1497" + }, + { + "label": "Strongfrom", + "value": "1502" + }, + { + "label": "Student", + "value": "1505" + }, + { + "label": "Sisters", + "value": "1511" + }, + { + "label": "Scriptwrit", + "value": "1519" + }, + { + "label": "Smuggling", + "value": "1547" + }, + { + "label": "StrongPowe", + "value": "1564" + }, + { + "label": "SweetYaoi", + "value": "1576" + }, + { + "label": "StrongOpFe", + "value": "1597" + }, + { + "label": "SaltedFish", + "value": "1610" + }, + { + "label": "Si-fi", + "value": "1616" + }, + { + "label": "SeaExplora", + "value": "1627" + }, + { + "label": "smokeinthe", + "value": "1641" + }, + { + "label": "SmokeCloud", + "value": "1650" + }, + { + "label": "Shallowsea", + "value": "1657" + }, + { + "label": "ServantofZ", + "value": "1667" + }, + { + "label": "shrimpinth", + "value": "1669" + }, + { + "label": "Scalesofth", + "value": "1675" + }, + { + "label": "shudder", + "value": "1679" + }, + { + "label": "sweetjelly", + "value": "1689" + }, + { + "label": "sadsword", + "value": "1695" + }, + { + "label": "Swordblood", + "value": "1697" + }, + { + "label": "SaltedFish", + "value": "1699" + }, + { + "label": "Shouldhand", + "value": "1710" + }, + { + "label": "Sterile", + "value": "1713" + }, + { + "label": "sundaysun", + "value": "1720" + }, + { + "label": "SuYechen", + "value": "1726" + }, + { + "label": "仕辰", + "value": "1727" + }, + { + "label": "sevenpigeo", + "value": "1729" + }, + { + "label": "summertree", + "value": "1736" + }, + { + "label": "summertrip", + "value": "1737" + }, + { + "label": "SuShaoqing", + "value": "1747" + }, + { + "label": "SwordImmor", + "value": "1749" + }, + { + "label": "sillycatse", + "value": "1759" + }, + { + "label": "six-twochi", + "value": "1764" + }, + { + "label": "Sadreminde", + "value": "1788" + }, + { + "label": "sleepslate", + "value": "1827" + }, + { + "label": "Scourge", + "value": "1833" + }, + { + "label": "ShenLuo", + "value": "1845" + }, + { + "label": "sleepingsa", + "value": "1855" + }, + { + "label": "SuperGodGr", + "value": "1869" + }, + { + "label": "stupidfox", + "value": "1873" + }, + { + "label": "snorkeling", + "value": "1889" + }, + { + "label": "spendthewo", + "value": "1894" + }, + { + "label": "showstory", + "value": "1902" + }, + { + "label": "Sixty-six", + "value": "1905" + }, + { + "label": "silentkill", + "value": "1910" + }, + { + "label": "ShenhuoxoR", + "value": "1923" + }, + { + "label": "sadsadness", + "value": "1937" + }, + { + "label": "sandrivere", + "value": "1964" + }, + { + "label": "startwriti", + "value": "1978" + }, + { + "label": "Siheyuanfl", + "value": "2033" + }, + { + "label": "SakuraMoon", + "value": "2064" + }, + { + "label": "Sevengener", + "value": "2067" + }, + { + "label": "Sword丨Lea", + "value": "2086" + }, + { + "label": "SiheyuanGo", + "value": "2097" + }, + { + "label": "SoulChef", + "value": "2106" + }, + { + "label": "SuZiyouyou", + "value": "2108" + }, + { + "label": "startofthe", + "value": "2116" + }, + { + "label": "Sweetandso", + "value": "2119" + }, + { + "label": "SuperPiran", + "value": "2129" + }, + { + "label": "SystemNo.3", + "value": "2132" + }, + { + "label": "SiheyuanDe", + "value": "2133" + }, + { + "label": "SaltedFish", + "value": "2156" + }, + { + "label": "shadowghos", + "value": "2159" + }, + { + "label": "scumteache", + "value": "2171" + }, + { + "label": "SkinButler", + "value": "2184" + }, + { + "label": "StinkBeanS", + "value": "2192" + }, + { + "label": "Silencehim", + "value": "2200" + }, + { + "label": "ShuYuChenX", + "value": "2203" + }, + { + "label": "StudentUni", + "value": "2212" + }, + { + "label": "specialwar", + "value": "2216" + }, + { + "label": "SillyColum", + "value": "2217" + }, + { + "label": "森罗", + "value": "2221" + }, + { + "label": "signinsalt", + "value": "2222" + }, + { + "label": "stardarkni", + "value": "2229" + }, + { + "label": "SuWei", + "value": "2232" + }, + { + "label": "self-disci", + "value": "2237" + }, + { + "label": "Simpleone", + "value": "2243" + }, + { + "label": "sisterisbe", + "value": "2246" + }, + { + "label": "Supernatur", + "value": "2250" + }, + { + "label": "SanmitheGr", + "value": "2259" + }, + { + "label": "Saltedfish", + "value": "2262" + }, + { + "label": "SoulCelest", + "value": "2270" + }, + { + "label": "soulanddre", + "value": "2281" + }, + { + "label": "secondpira", + "value": "2285" + }, + { + "label": "SixPathsof", + "value": "2305" + }, + { + "label": "stevec", + "value": "2320" + }, + { + "label": "StewedChic", + "value": "2328" + }, + { + "label": "Secretobse", + "value": "2338" + }, + { + "label": "sleeplesst", + "value": "2344" + }, + { + "label": "SuperGodNo", + "value": "2352" + }, + { + "label": "Superfire", + "value": "2366" + }, + { + "label": "Stomachhur", + "value": "2368" + }, + { + "label": "SongoftheG", + "value": "2380" + }, + { + "label": "stablefort", + "value": "2385" + }, + { + "label": "stonemored", + "value": "2386" + }, + { + "label": "soulmemory", + "value": "2391" + }, + { + "label": "Straightme", + "value": "2404" + }, + { + "label": "ShenJin", + "value": "2419" + }, + { + "label": "Smallmushr", + "value": "2423" + }, + { + "label": "sistercook", + "value": "2428" + }, + { + "label": "ShenhaoMec", + "value": "2436" + }, + { + "label": "singlesalt", + "value": "2448" + }, + { + "label": "summernow", + "value": "2453" + }, + { + "label": "swordrepai", + "value": "2493" + }, + { + "label": "Sakurajima", + "value": "2518" + }, + { + "label": "Smokebambo", + "value": "2522" + }, + { + "label": "Sencha", + "value": "2540" + }, + { + "label": "starfish", + "value": "2546" + }, + { + "label": "swearnotto", + "value": "2551" + }, + { + "label": "SaltedFish", + "value": "2557" + }, + { + "label": "Swimmingfi", + "value": "2562" + }, + { + "label": "speechless", + "value": "2570" + }, + { + "label": "supernovab", + "value": "2608" + }, + { + "label": "SwingingDe", + "value": "2614" + }, + { + "label": "SacrificeX", + "value": "2619" + }, + { + "label": "sopoor", + "value": "2621" + }, + { + "label": "softorange", + "value": "2634" + }, + { + "label": "sunsetover", + "value": "2638" + }, + { + "label": "streamerbl", + "value": "2639" + }, + { + "label": "SouthKefei", + "value": "2651" + }, + { + "label": "stopatfirs", + "value": "2679" + }, + { + "label": "shadowfall", + "value": "2680" + }, + { + "label": "silver", + "value": "2694" + }, + { + "label": "Science-fi", + "value": "2731" + }, + { + "label": "Stand", + "value": "2742" + }, + { + "label": "Show-biz", + "value": "2746" + }, + { + "label": "SxualAbuse", + "value": "2758" + }, + { + "label": "SonInLaw", + "value": "2776" + }, + { + "label": "Shadowless", + "value": "2789" + }, + { + "label": "Superman", + "value": "2793" + }, + { + "label": "Senbeiboy", + "value": "2799" + }, + { + "label": "Sugary", + "value": "2809" + }, + { + "label": "Starwars", + "value": "2815" + }, + { + "label": "Sect", + "value": "2825" + }, + { + "label": "SelfDiscip", + "value": "2826" + }, + { + "label": "Smart", + "value": "2829" + }, + { + "label": "steamponk", + "value": "2831" + }, + { + "label": "systemmale", + "value": "2842" + }, + { + "label": "Softyander", + "value": "2861" + }, + { + "label": "Slvery", + "value": "2907" + }, + { + "label": "skycity", + "value": "2923" + }, + { + "label": "strongwoma", + "value": "2928" + }, + { + "label": "specialpow", + "value": "2951" + }, + { + "label": "Starships", + "value": "2964" + }, + { + "label": "Suddenmarr", + "value": "2989" + }, + { + "label": "Strongfl", + "value": "2996" + }, + { + "label": "Schizophre", + "value": "3000" + }, + { + "label": "Swodandmag", + "value": "3004" + }, + { + "label": "Secretive", + "value": "3011" + }, + { + "label": "SuperAbili", + "value": "3012" + }, + { + "label": "Stepmom", + "value": "3039" + }, + { + "label": "Sweetpampe", + "value": "3055" + }, + { + "label": "Sobrenatur", + "value": "3057" + }, + { + "label": "Stealth", + "value": "3100" + }, + { + "label": "SlowPaced", + "value": "3101" + }, + { + "label": "Studenttea", + "value": "3107" + }, + { + "label": "specialage", + "value": "3123" + }, + { + "label": "Swordsandm", + "value": "3127" + }, + { + "label": "Sorcery", + "value": "3149" + }, + { + "label": "Steampunk", + "value": "3156" + }, + { + "label": "Steamy", + "value": "3169" + }, + { + "label": "Smartfemal", + "value": "3170" + }, + { + "label": "Sekai", + "value": "3177" + }, + { + "label": "Slim", + "value": "3183" + }, + { + "label": "ShamelessM", + "value": "3187" + }, + { + "label": "Scheming", + "value": "3189" + }, + { + "label": "Summonerpr", + "value": "3195" + }, + { + "label": "Sliceoflif", + "value": "3196" + }, + { + "label": "Supporting", + "value": "3217" + }, + { + "label": "Sectbuildi", + "value": "3227" + }, + { + "label": "Sciencefic", + "value": "3230" + }, + { + "label": "Seinen", + "value": "3241" + }, + { + "label": "Transmigra", + "value": "49" + }, + { + "label": "TimeTravel", + "value": "296" + }, + { + "label": "Talents", + "value": "658" + }, + { + "label": "Tragedy", + "value": "730" + }, + { + "label": "TimeSkip", + "value": "254" + }, + { + "label": "TragicPast", + "value": "128" + }, + { + "label": "Technologi", + "value": "498" + }, + { + "label": "Tsundere", + "value": "130" + }, + { + "label": "Thestronga", + "value": "2960" + }, + { + "label": "Thriller", + "value": "601" + }, + { + "label": "Twins", + "value": "372" + }, + { + "label": "Teamwork", + "value": "113" + }, + { + "label": "TimeManipu", + "value": "466" + }, + { + "label": "Thieves", + "value": "770" + }, + { + "label": "Teachers", + "value": "185" + }, + { + "label": "TribalSoci", + "value": "280" + }, + { + "label": "Transplant", + "value": "319" + }, + { + "label": "TwistedPer", + "value": "731" + }, + { + "label": "TomboyishF", + "value": "258" + }, + { + "label": "Threesome", + "value": "507" + }, + { + "label": "TimeLoop", + "value": "567" + }, + { + "label": "Transmigat", + "value": "1221" + }, + { + "label": "Talent", + "value": "1442" + }, + { + "label": "TS", + "value": "2904" + }, + { + "label": "Trap", + "value": "129" + }, + { + "label": "Tennis", + "value": "301" + }, + { + "label": "TerritoryC", + "value": "681" + }, + { + "label": "Toriko", + "value": "690" + }, + { + "label": "TopMC", + "value": "1255" + }, + { + "label": "twilight", + "value": "1817" + }, + { + "label": "Twisted", + "value": "3028" + }, + { + "label": "TimidProta", + "value": "278" + }, + { + "label": "TerminalIl", + "value": "445" + }, + { + "label": "Torture", + "value": "870" + }, + { + "label": "Titans", + "value": "925" + }, + { + "label": "Transmigra", + "value": "1100" + }, + { + "label": "travel", + "value": "2924" + }, + { + "label": "Teen", + "value": "2986" + }, + { + "label": "Transmigra", + "value": "642" + }, + { + "label": "Traverse", + "value": "821" + }, + { + "label": "team", + "value": "941" + }, + { + "label": "Technology", + "value": "1029" + }, + { + "label": "Taoist", + "value": "1047" + }, + { + "label": "Tensura", + "value": "1194" + }, + { + "label": "TypeMoon", + "value": "1195" + }, + { + "label": "Transmigra", + "value": "1217" + }, + { + "label": "Tasker", + "value": "1235" + }, + { + "label": "Trade", + "value": "1498" + }, + { + "label": "Transmigra", + "value": "1548" + }, + { + "label": "TangJichen", + "value": "1704" + }, + { + "label": "Time-Trave", + "value": "1791" + }, + { + "label": "TsukibaAki", + "value": "1993" + }, + { + "label": "Transporte", + "value": "279" + }, + { + "label": "TableTenni", + "value": "300" + }, + { + "label": "Transporte", + "value": "320" + }, + { + "label": "Transporte", + "value": "349" + }, + { + "label": "Transforma", + "value": "602" + }, + { + "label": "Transporte", + "value": "630" + }, + { + "label": "teacher", + "value": "635" + }, + { + "label": "TreasureHu", + "value": "776" + }, + { + "label": "Technology", + "value": "828" + }, + { + "label": "TravelingT", + "value": "835" + }, + { + "label": "Talismans", + "value": "894" + }, + { + "label": "TowerDefen", + "value": "897" + }, + { + "label": "ThreeKingd", + "value": "947" + }, + { + "label": "Tokyo", + "value": "1010" + }, + { + "label": "TimeandSpa", + "value": "1084" + }, + { + "label": "TalentShow", + "value": "1109" + }, + { + "label": "Trnasmigra", + "value": "1151" + }, + { + "label": "Traveling", + "value": "1171" + }, + { + "label": "Tramsmigra", + "value": "1204" + }, + { + "label": "traveller", + "value": "1232" + }, + { + "label": "Tsuru", + "value": "1292" + }, + { + "label": "Tailsman", + "value": "1314" + }, + { + "label": "TsundereLo", + "value": "1325" + }, + { + "label": "TerritoryM", + "value": "1330" + }, + { + "label": "TokyoGhoul", + "value": "1351" + }, + { + "label": "Tyrant", + "value": "1385" + }, + { + "label": "Terrorists", + "value": "1430" + }, + { + "label": "Transporte", + "value": "1492" + }, + { + "label": "Tranformer", + "value": "1493" + }, + { + "label": "Transmigra", + "value": "1528" + }, + { + "label": "TreasureHu", + "value": "1579" + }, + { + "label": "Transmigra", + "value": "1585" + }, + { + "label": "TreasureHu", + "value": "1588" + }, + { + "label": "Tianbang78", + "value": "1636" + }, + { + "label": "Thesunsett", + "value": "1661" + }, + { + "label": "Theflowero", + "value": "1662" + }, + { + "label": "Threedaysa", + "value": "1705" + }, + { + "label": "Twilightis", + "value": "1708" + }, + { + "label": "TheGospelo", + "value": "1711" + }, + { + "label": "TheThreeKi", + "value": "1719" + }, + { + "label": "TingFengZh", + "value": "1739" + }, + { + "label": "tomorrowwi", + "value": "1741" + }, + { + "label": "Three-flav", + "value": "1763" + }, + { + "label": "Thelightof", + "value": "1770" + }, + { + "label": "TaurenIron", + "value": "1780" + }, + { + "label": "Tenthousan", + "value": "1790" + }, + { + "label": "TempleThir", + "value": "1800" + }, + { + "label": "ThreshingG", + "value": "1801" + }, + { + "label": "Technology", + "value": "1820" + }, + { + "label": "Thunderous", + "value": "1826" + }, + { + "label": "thegodofde", + "value": "1839" + }, + { + "label": "ToneMasaya", + "value": "1860" + }, + { + "label": "Thebiggest", + "value": "1868" + }, + { + "label": "Thebigdevi", + "value": "1871" + }, + { + "label": "TopoftheCl", + "value": "1878" + }, + { + "label": "thisyear", + "value": "1886" + }, + { + "label": "Thenewbact", + "value": "1917" + }, + { + "label": "ThreeLives", + "value": "1920" + }, + { + "label": "thewindisb", + "value": "1930" + }, + { + "label": "铁帅", + "value": "1945" + }, + { + "label": "TopoftheCl", + "value": "1946" + }, + { + "label": "Thestronge", + "value": "1953" + }, + { + "label": "TeckTyrann", + "value": "1957" + }, + { + "label": "Theoceando", + "value": "1966" + }, + { + "label": "thaw", + "value": "1967" + }, + { + "label": "therearefi", + "value": "2000" + }, + { + "label": "Tianbangth", + "value": "2006" + }, + { + "label": "TheGodfath", + "value": "2018" + }, + { + "label": "TenCommand", + "value": "2021" + }, + { + "label": "takestock", + "value": "2028" + }, + { + "label": "tobacco", + "value": "2036" + }, + { + "label": "Thinkingof", + "value": "2046" + }, + { + "label": "Theworld&a", + "value": "2055" + }, + { + "label": "threelittl", + "value": "2059" + }, + { + "label": "TombRaider", + "value": "2069" + }, + { + "label": "Tianbangol", + "value": "2074" + }, + { + "label": "TangShaoqi", + "value": "2076" + }, + { + "label": "Theancesto", + "value": "2114" + }, + { + "label": "Thequeenis", + "value": "2157" + }, + { + "label": "Thetruegod", + "value": "2160" + }, + { + "label": "TianYiding", + "value": "2167" + }, + { + "label": "TianbangYa", + "value": "2169" + }, + { + "label": "Thirty-two", + "value": "2177" + }, + { + "label": "Tianshitak", + "value": "2194" + }, + { + "label": "TombRaider", + "value": "2205" + }, + { + "label": "Theoldfive", + "value": "2207" + }, + { + "label": "Two-dimens", + "value": "2224" + }, + { + "label": "threeteeth", + "value": "2230" + }, + { + "label": "TwentyFame", + "value": "2236" + }, + { + "label": "Thelistisi", + "value": "2240" + }, + { + "label": "ThousandTe", + "value": "2258" + }, + { + "label": "takeoverth", + "value": "2269" + }, + { + "label": "TopoftheFo", + "value": "2274" + }, + { + "label": "Theashesar", + "value": "2277" + }, + { + "label": "TombRaider", + "value": "2284" + }, + { + "label": "Two-dimens", + "value": "2287" + }, + { + "label": "Teemotofly", + "value": "2299" + }, + { + "label": "TombRaider", + "value": "2301" + }, + { + "label": "TrumanLive", + "value": "2309" + }, + { + "label": "Takeaplane", + "value": "2314" + }, + { + "label": "Thecatisgo", + "value": "2322" + }, + { + "label": "Tsunderesc", + "value": "2333" + }, + { + "label": "Thefishmar", + "value": "2357" + }, + { + "label": "ThreeLives", + "value": "2376" + }, + { + "label": "Thousandso", + "value": "2377" + }, + { + "label": "Tigerteeth", + "value": "2396" + }, + { + "label": "TroubledWo", + "value": "2408" + }, + { + "label": "Thepowerof", + "value": "2413" + }, + { + "label": "TimeKingJO", + "value": "2433" + }, + { + "label": "Thankyoufo", + "value": "2438" + }, + { + "label": "TianbangHu", + "value": "2475" + }, + { + "label": "Two-dimens", + "value": "2490" + }, + { + "label": "TenThousan", + "value": "2501" + }, + { + "label": "takeoffboy", + "value": "2511" + }, + { + "label": "Twistbroth", + "value": "2525" + }, + { + "label": "towashthed", + "value": "2536" + }, + { + "label": "Thegloryof", + "value": "2542" + }, + { + "label": "Twopoundso", + "value": "2552" + }, + { + "label": "Today&0", + "value": "2578" + }, + { + "label": "ThreeDotIn", + "value": "2591" + }, + { + "label": "Twopeopleb", + "value": "2598" + }, + { + "label": "Toilet", + "value": "2605" + }, + { + "label": "TopoftheCl", + "value": "2615" + }, + { + "label": "Tianbanggr", + "value": "2622" + }, + { + "label": "Thelistdep", + "value": "2627" + }, + { + "label": "Thewayofth", + "value": "2641" + }, + { + "label": "Thisissure", + "value": "2647" + }, + { + "label": "twingods", + "value": "2648" + }, + { + "label": "twilightdr", + "value": "2649" + }, + { + "label": "TeenageXia", + "value": "2665" + }, + { + "label": "treeofenli", + "value": "2686" + }, + { + "label": "Thetopofth", + "value": "2724" + }, + { + "label": "TangThirty", + "value": "2725" + }, + { + "label": "TrueorFake", + "value": "2772" + }, + { + "label": "TianbangDi", + "value": "2800" + }, + { + "label": "teacher-st", + "value": "2839" + }, + { + "label": "two-wayred", + "value": "2843" + }, + { + "label": "Timelimit", + "value": "2855" + }, + { + "label": "Turtle", + "value": "2877" + }, + { + "label": "Thiller", + "value": "2889" + }, + { + "label": "teachermal", + "value": "2952" + }, + { + "label": "Trialmarri", + "value": "3001" + }, + { + "label": "Terrori", + "value": "3013" + }, + { + "label": "TheMainCha", + "value": "3019" + }, + { + "label": "TheDevil", + "value": "3020" + }, + { + "label": "Tsundereml", + "value": "3050" + }, + { + "label": "Transmigat", + "value": "3051" + }, + { + "label": "Trickster", + "value": "3058" + }, + { + "label": "Tiger", + "value": "3087" + }, + { + "label": "Trauma", + "value": "3193" + }, + { + "label": "Urban", + "value": "131" + }, + { + "label": "UnlimitedF", + "value": "1123" + }, + { + "label": "UglytoBeau", + "value": "305" + }, + { + "label": "Unconditio", + "value": "725" + }, + { + "label": "UnluckyPro", + "value": "345" + }, + { + "label": "Undead", + "value": "2919" + }, + { + "label": "UrbanLife", + "value": "733" + }, + { + "label": "Unprincipl", + "value": "3053" + }, + { + "label": "Unrequited", + "value": "535" + }, + { + "label": "Urbanroman", + "value": "3228" + }, + { + "label": "UglyProtag", + "value": "1414" + }, + { + "label": "Unreliable", + "value": "265" + }, + { + "label": "UniqueWeap", + "value": "1428" + }, + { + "label": "UniqueCult", + "value": "78" + }, + { + "label": "Underestim", + "value": "373" + }, + { + "label": "Urbanyouth", + "value": "1333" + }, + { + "label": "UniqueWeap", + "value": "1456" + }, + { + "label": "Undocument", + "value": "1654" + }, + { + "label": "Unknowntea", + "value": "1683" + }, + { + "label": "UrbanDatan", + "value": "1728" + }, + { + "label": "UltramanPo", + "value": "1785" + }, + { + "label": "unbearable", + "value": "1897" + }, + { + "label": "undeadfish", + "value": "1977" + }, + { + "label": "Upsetting", + "value": "2040" + }, + { + "label": "urbanstar", + "value": "2066" + }, + { + "label": "Undead丨Kn", + "value": "2071" + }, + { + "label": "urbanshark", + "value": "2113" + }, + { + "label": "UnknownTao", + "value": "2276" + }, + { + "label": "undersilve", + "value": "2290" + }, + { + "label": "Undefeated", + "value": "2358" + }, + { + "label": "UrbanMilit", + "value": "2379" + }, + { + "label": "unknown", + "value": "2394" + }, + { + "label": "underlolic", + "value": "2583" + }, + { + "label": "Unintentio", + "value": "2704" + }, + { + "label": "understate", + "value": "2715" + }, + { + "label": "Unlimitedc", + "value": "2723" + }, + { + "label": "Urbanyearn", + "value": "2726" + }, + { + "label": "Unique", + "value": "2866" + }, + { + "label": "Unlucky", + "value": "2875" + }, + { + "label": "Ugly", + "value": "2899" + }, + { + "label": "Upgrade", + "value": "3154" + }, + { + "label": "UrbanRoman", + "value": "3239" + }, + { + "label": "Villain", + "value": "512" + }, + { + "label": "VirtualRea", + "value": "162" + }, + { + "label": "Videogame", + "value": "3060" + }, + { + "label": "Vampire", + "value": "1253" + }, + { + "label": "Vampires", + "value": "186" + }, + { + "label": "Villainess", + "value": "811" + }, + { + "label": "VillainPro", + "value": "1241" + }, + { + "label": "VoiceActor", + "value": "1423" + }, + { + "label": "Villainess", + "value": "1224" + }, + { + "label": "Video-Game", + "value": "3161" + }, + { + "label": "VoicePack", + "value": "665" + }, + { + "label": "Villains", + "value": "1146" + }, + { + "label": "VarietySho", + "value": "1250" + }, + { + "label": "VictorianE", + "value": "1551" + }, + { + "label": "videogames", + "value": "2909" + }, + { + "label": "Vrmmo", + "value": "3176" + }, + { + "label": "VillainEvi", + "value": "670" + }, + { + "label": "VillIain", + "value": "1089" + }, + { + "label": "Vest", + "value": "1092" + }, + { + "label": "Viewofthec", + "value": "1825" + }, + { + "label": "vampiredri", + "value": "2374" + }, + { + "label": "VikaBaka", + "value": "2802" + }, + { + "label": "Violence", + "value": "2908" + }, + { + "label": "Vrmmorpg", + "value": "3175" + }, + { + "label": "Villainous", + "value": "3184" + }, + { + "label": "Villainmc", + "value": "3197" + }, + { + "label": "Villainher", + "value": "3198" + }, + { + "label": "WeaktoStro", + "value": "32" + }, + { + "label": "WorldHoppi", + "value": "273" + }, + { + "label": "WealthyCha", + "value": "7" + }, + { + "label": "Wizards", + "value": "391" + }, + { + "label": "WorldTrave", + "value": "218" + }, + { + "label": "Wars", + "value": "291" + }, + { + "label": "Writers", + "value": "404" + }, + { + "label": "Werewolf", + "value": "2978" + }, + { + "label": "WeakProtag", + "value": "497" + }, + { + "label": "Wizard", + "value": "706" + }, + { + "label": "Witches", + "value": "437" + }, + { + "label": "War", + "value": "1076" + }, + { + "label": "Wuxia", + "value": "425" + }, + { + "label": "WesternFan", + "value": "993" + }, + { + "label": "World-hopp", + "value": "812" + }, + { + "label": "Werebeasts", + "value": "857" + }, + { + "label": "WorldofWar", + "value": "1304" + }, + { + "label": "Wishes", + "value": "348" + }, + { + "label": "Wisdom", + "value": "3212" + }, + { + "label": "WarsWeakto", + "value": "514" + }, + { + "label": "WearBook", + "value": "742" + }, + { + "label": "Warhammer4", + "value": "1296" + }, + { + "label": "WorldTree", + "value": "1467" + }, + { + "label": "Witch", + "value": "2847" + }, + { + "label": "Wasteland", + "value": "1030" + }, + { + "label": "Wealth", + "value": "1130" + }, + { + "label": "Writer", + "value": "1499" + }, + { + "label": "Wilderness", + "value": "1562" + }, + { + "label": "WenXuanyu", + "value": "1995" + }, + { + "label": "WW2", + "value": "2838" + }, + { + "label": "Wholesome", + "value": "2860" + }, + { + "label": "wealthyfam", + "value": "2945" + }, + { + "label": "WeaktoClan", + "value": "676" + }, + { + "label": "WorldEmpir", + "value": "691" + }, + { + "label": "WorldWar2", + "value": "1077" + }, + { + "label": "Warship", + "value": "1172" + }, + { + "label": "WealthyCha", + "value": "1173" + }, + { + "label": "WealthChar", + "value": "1205" + }, + { + "label": "Wearabook", + "value": "1216" + }, + { + "label": "Warlocks", + "value": "1297" + }, + { + "label": "wearingabo", + "value": "1320" + }, + { + "label": "Wealthy", + "value": "1598" + }, + { + "label": "weektoStro", + "value": "1607" + }, + { + "label": "wishardtow", + "value": "1672" + }, + { + "label": "writeonlyz", + "value": "1687" + }, + { + "label": "WangJiu", + "value": "1694" + }, + { + "label": "Wuxicheng", + "value": "1703" + }, + { + "label": "WindSpirit", + "value": "1786" + }, + { + "label": "wanderings", + "value": "1815" + }, + { + "label": "What&03", + "value": "1874" + }, + { + "label": "Wanderer", + "value": "1940" + }, + { + "label": "Westernrai", + "value": "1970" + }, + { + "label": "windingpat", + "value": "1974" + }, + { + "label": "witchfan", + "value": "2001" + }, + { + "label": "watermelon", + "value": "2024" + }, + { + "label": "WasteWoodA", + "value": "2025" + }, + { + "label": "whitekeybo", + "value": "2101" + }, + { + "label": "Winningthe", + "value": "2144" + }, + { + "label": "Walkinthec", + "value": "2152" + }, + { + "label": "Whitehorse", + "value": "2206" + }, + { + "label": "WangXiaomi", + "value": "2213" + }, + { + "label": "witheredpr", + "value": "2273" + }, + { + "label": "Wuhutookof", + "value": "2294" + }, + { + "label": "willowcand", + "value": "2304" + }, + { + "label": "wastefish", + "value": "2308" + }, + { + "label": "WangEr", + "value": "2317" + }, + { + "label": "wanttocome", + "value": "2356" + }, + { + "label": "wanttoeatg", + "value": "2381" + }, + { + "label": "WenGuang", + "value": "2417" + }, + { + "label": "WifeistheD", + "value": "2464" + }, + { + "label": "watertown", + "value": "2470" + }, + { + "label": "warmtime", + "value": "2484" + }, + { + "label": "windandmap", + "value": "2497" + }, + { + "label": "Whoringmak", + "value": "2596" + }, + { + "label": "whiteshirt", + "value": "2682" + }, + { + "label": "WOW", + "value": "2753" + }, + { + "label": "WarofCivil", + "value": "2765" + }, + { + "label": "Warlock", + "value": "2813" + }, + { + "label": "wife-chasi", + "value": "2872" + }, + { + "label": "Warcraft", + "value": "2938" + }, + { + "label": "Worldbuild", + "value": "2965" + }, + { + "label": "Worlds", + "value": "2992" + }, + { + "label": "Weakstrong", + "value": "3049" + }, + { + "label": "Worldofchu", + "value": "3082" + }, + { + "label": "Westerngod", + "value": "3150" + }, + { + "label": "Worldtrave", + "value": "3160" + }, + { + "label": "Weak-to-st", + "value": "3178" + }, + { + "label": "Weakstrong", + "value": "3199" + }, + { + "label": "WeaktoStro", + "value": "3210" + }, + { + "label": "Xianxia", + "value": "709" + }, + { + "label": "Xuanhuan", + "value": "487" + }, + { + "label": "XiuXiuXiuX", + "value": "1700" + }, + { + "label": "谢邀", + "value": "1754" + }, + { + "label": "Xiaoxin", + "value": "1795" + }, + { + "label": "Xueqiunder", + "value": "1918" + }, + { + "label": "小封", + "value": "2052" + }, + { + "label": "XuebaIII", + "value": "2098" + }, + { + "label": "Xufamilyel", + "value": "2099" + }, + { + "label": "Xuebaisinv", + "value": "2135" + }, + { + "label": "XuIintheTa", + "value": "2331" + }, + { + "label": "Xiaothreey", + "value": "2351" + }, + { + "label": "XieDaoheng", + "value": "2393" + }, + { + "label": "Xiaonianbl", + "value": "2406" + }, + { + "label": "XiaonianXu", + "value": "2500" + }, + { + "label": "XiaomiStar", + "value": "2523" + }, + { + "label": "Xiaosaid", + "value": "2573" + }, + { + "label": "XiaoxiangP", + "value": "2714" + }, + { + "label": "Yandere", + "value": "304" + }, + { + "label": "Yuri", + "value": "773" + }, + { + "label": "YoungerSis", + "value": "33" + }, + { + "label": "Yaoi", + "value": "1349" + }, + { + "label": "Yu-Gi-Oh!", + "value": "3244" + }, + { + "label": "YoungerLov", + "value": "728" + }, + { + "label": "YoungerBro", + "value": "1431" + }, + { + "label": "Youth", + "value": "1560" + }, + { + "label": "YeluChengj", + "value": "1678" + }, + { + "label": "Yugioh", + "value": "1391" + }, + { + "label": "Yu-Gi-Oh", + "value": "1614" + }, + { + "label": "YellowSpri", + "value": "1638" + }, + { + "label": "youaretoow", + "value": "1750" + }, + { + "label": "YinLiisins", + "value": "1772" + }, + { + "label": "Yongchuang", + "value": "1835" + }, + { + "label": "YingXiaofe", + "value": "1881" + }, + { + "label": "YangXiaoA", + "value": "1915" + }, + { + "label": "YuXiaoqi", + "value": "1997" + }, + { + "label": "Yearningfo", + "value": "2050" + }, + { + "label": "yearningfo", + "value": "2084" + }, + { + "label": "Yunmu", + "value": "2090" + }, + { + "label": "Ying&03", + "value": "2131" + }, + { + "label": "YuboTiandi", + "value": "2136" + }, + { + "label": "Yakult", + "value": "2139" + }, + { + "label": "YeXiaobai", + "value": "2141" + }, + { + "label": "Yearningfo", + "value": "2218" + }, + { + "label": "Yaoyue", + "value": "2355" + }, + { + "label": "YuTsingYi", + "value": "2397" + }, + { + "label": "yearningfo", + "value": "2426" + }, + { + "label": "YeGucheng", + "value": "2526" + }, + { + "label": "YoungMaste", + "value": "2555" + }, + { + "label": "YeGongzi", + "value": "2561" + }, + { + "label": "YuYuyu", + "value": "2569" + }, + { + "label": "yearningfo", + "value": "2624" + }, + { + "label": "YoungMaste", + "value": "2637" + }, + { + "label": "YeQianqiu", + "value": "2645" + }, + { + "label": "yearaftery", + "value": "2705" + }, + { + "label": "YunZhongju", + "value": "2717" + }, + { + "label": "Yearningto", + "value": "2782" + }, + { + "label": "YeYe", + "value": "2790" + }, + { + "label": "Yamen", + "value": "2876" + }, + { + "label": "younglovei", + "value": "2903" + }, + { + "label": "YoungGirl", + "value": "3173" + }, + { + "label": "Ystem", + "value": "3188" + }, + { + "label": "Young", + "value": "3204" + }, + { + "label": "Zombies", + "value": "205" + }, + { + "label": "Zergs", + "value": "800" + }, + { + "label": "Zerg", + "value": "1051" + }, + { + "label": "Zombie", + "value": "1175" + }, + { + "label": "z-man", + "value": "623" + }, + { + "label": "ZhuZhiyue", + "value": "1981" + }, + { + "label": "ZombieQuee", + "value": "1179" + }, + { + "label": "Zoo", + "value": "1566" + }, + { + "label": "ZiXuanXuan", + "value": "1753" + }, + { + "label": "ZombieGod", + "value": "2008" + }, + { + "label": "Zuge", + "value": "2011" + }, + { + "label": "ZhugeIrona", + "value": "2022" + }, + { + "label": "zombiefish", + "value": "2115" + }, + { + "label": "Zulongstil", + "value": "2121" + }, + { + "label": "ZhangTianb", + "value": "2193" + }, + { + "label": "ZhuDabald", + "value": "2362" + }, + { + "label": "ZhangJuli", + "value": "2425" + }, + { + "label": "ZhangFeiin", + "value": "2449" + }, + { + "label": "ZhugeDali&", + "value": "2458" + }, + { + "label": "Zhugeiscra", + "value": "2466" + }, + { + "label": "ZhangErgou", + "value": "2530" + }, + { + "label": "ZuwuGonggo", + "value": "2549" + }, + { + "label": "zhishen", + "value": "2577" + }, + { + "label": "Zippo", + "value": "2586" + }, + { + "label": "ZombieSumo", + "value": "2806" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readwn/filters/wuxiap.json b/plugins/multisrc/readwn/filters/wuxiap.json new file mode 100644 index 000000000..f6e41f17f --- /dev/null +++ b/plugins/multisrc/readwn/filters/wuxiap.json @@ -0,0 +1,24888 @@ +{ + "filters": { + "sort": { + "type": "Picker", + "label": "Sort By", + "value": "onclick", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ] + }, + "status": { + "type": "Picker", + "label": "Status", + "value": "all", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ] + }, + "genres": { + "type": "Picker", + "label": "Genre / Category", + "value": "", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Chinese", + "value": "chinese" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Erciyuan", + "value": "erciyuan" + }, + { + "label": "Faloo", + "value": "faloo" + }, + { + "label": "Fan-Fiction", + "value": "fan-fiction" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Game", + "value": "game" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Hentai", + "value": "hentai" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Isekai", + "value": "isekai" + }, + { + "label": "Japanese", + "value": "japanese" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Korean", + "value": "korean" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magic", + "value": "magic" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Military", + "value": "military" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Official Circles", + "value": "official_circles" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Science Fiction", + "value": "science_fiction" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shoujo Ai", + "value": "shoujo-ai" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Suspense Thriller", + "value": "suspense_thriller" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Travel Through Time", + "value": "travel_through_time" + }, + { + "label": "Two-dimensional", + "value": "two-dimensional" + }, + { + "label": "Urban", + "value": "urban" + }, + { + "label": "Urban Life", + "value": "urban-life" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Virtual Reality", + "value": "virtual-reality" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Wuxia Xianxia", + "value": "wuxia_xianxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + }, + { + "label": "Yuri", + "value": "yuri" + } + ] + }, + "tags": { + "type": "Picker", + "label": "Tags", + "value": "", + "options": [ + { + "label": "NONE", + "value": "" + }, + { + "label": "Ability", + "value": "1203" + }, + { + "label": "Academy", + "value": "50" + }, + { + "label": "AncientChi", + "value": "34" + }, + { + "label": "Apocalypse", + "value": "89" + }, + { + "label": "Alchemy", + "value": "52" + }, + { + "label": "ArrogantCh", + "value": "55" + }, + { + "label": "Action", + "value": "90" + }, + { + "label": "AncientTim", + "value": "149" + }, + { + "label": "Adventure", + "value": "943" + }, + { + "label": "Acting", + "value": "103" + }, + { + "label": "AlternateW", + "value": "220" + }, + { + "label": "AdaptedtoM", + "value": "88" + }, + { + "label": "ArrangedMa", + "value": "223" + }, + { + "label": "ArmyBuildi", + "value": "163" + }, + { + "label": "Alien", + "value": "3111" + }, + { + "label": "Artifacts", + "value": "210" + }, + { + "label": "AntiheroPr", + "value": "36" + }, + { + "label": "Aristocrac", + "value": "221" + }, + { + "label": "Amnesia", + "value": "244" + }, + { + "label": "Assassins", + "value": "217" + }, + { + "label": "AncientSpi", + "value": "3235" + }, + { + "label": "AgeProgres", + "value": "206" + }, + { + "label": "Almighty", + "value": "3265" + }, + { + "label": "Abuse", + "value": "856" + }, + { + "label": "Adventurer", + "value": "276" + }, + { + "label": "Aliens", + "value": "243" + }, + { + "label": "Anti-routi", + "value": "3096" + }, + { + "label": "AbsentPare", + "value": "240" + }, + { + "label": "AlternateH", + "value": "1256" + }, + { + "label": "AdaptedtoD", + "value": "146" + }, + { + "label": "Army", + "value": "329" + }, + { + "label": "AbilitySte", + "value": "289" + }, + { + "label": "Accelerate", + "value": "51" + }, + { + "label": "Afterthesh", + "value": "3231" + }, + { + "label": "AbusiveCha", + "value": "1" + }, + { + "label": "Appearance", + "value": "54" + }, + { + "label": "ArtifactCr", + "value": "209" + }, + { + "label": "AdaptedtoM", + "value": "239" + }, + { + "label": "AbandonedC", + "value": "145" + }, + { + "label": "Agent", + "value": "3605" + }, + { + "label": "Angels", + "value": "369" + }, + { + "label": "AdoptedPro", + "value": "270" + }, + { + "label": "ApatheticP", + "value": "295" + }, + { + "label": "AgeRegress", + "value": "436" + }, + { + "label": "Anti-Japan", + "value": "3269" + }, + { + "label": "AdaptedtoA", + "value": "87" + }, + { + "label": "abandonedw", + "value": "3327" + }, + { + "label": "AdoptedChi", + "value": "262" + }, + { + "label": "AntiHero", + "value": "816" + }, + { + "label": "AdaptedtoM", + "value": "677" + }, + { + "label": "Archery", + "value": "411" + }, + { + "label": "Aggressive", + "value": "242" + }, + { + "label": "ancientlov", + "value": "5680" + }, + { + "label": "AdaptedtoD", + "value": "495" + }, + { + "label": "Adultery", + "value": "433" + }, + { + "label": "ancientwil", + "value": "3432" + }, + { + "label": "Army-build", + "value": "870" + }, + { + "label": "Aristocrat", + "value": "1150" + }, + { + "label": "airflow", + "value": "3266" + }, + { + "label": "America", + "value": "3285" + }, + { + "label": "Aresstream", + "value": "3214" + }, + { + "label": "AncientArm", + "value": "5701" + }, + { + "label": "ABO", + "value": "930" + }, + { + "label": "Anime", + "value": "759" + }, + { + "label": "Artists", + "value": "302" + }, + { + "label": "Androids", + "value": "471" + }, + { + "label": "AnimalRear", + "value": "453" + }, + { + "label": "Anl", + "value": "189" + }, + { + "label": "athlete", + "value": "3275" + }, + { + "label": "AwkwardPro", + "value": "271" + }, + { + "label": "Autism", + "value": "241" + }, + { + "label": "Afunnyguy", + "value": "3697" + }, + { + "label": "Affair", + "value": "670" + }, + { + "label": "AdaptedtoG", + "value": "334" + }, + { + "label": "Anal", + "value": "53" + }, + { + "label": "AggresiveC", + "value": "1284" + }, + { + "label": "AnotherWor", + "value": "899" + }, + { + "label": "AdaptedtoM", + "value": "147" + }, + { + "label": "Alienbeast", + "value": "6186" + }, + { + "label": "Avatar", + "value": "3698" + }, + { + "label": "Academystr", + "value": "6155" + }, + { + "label": "Ancestralf", + "value": "3352" + }, + { + "label": "Archer", + "value": "2590" + }, + { + "label": "ArmsDealer", + "value": "357" + }, + { + "label": "ArtifactsC", + "value": "56" + }, + { + "label": "ancientwei", + "value": "3637" + }, + { + "label": "Adult", + "value": "1448" + }, + { + "label": "Alternativ", + "value": "3836" + }, + { + "label": "AgeGap", + "value": "1642" + }, + { + "label": "AntiqueSho", + "value": "619" + }, + { + "label": "ancestry", + "value": "5826" + }, + { + "label": "ArrayMage", + "value": "4344" + }, + { + "label": "Azeroth", + "value": "1702" + }, + { + "label": "advancedte", + "value": "1661" + }, + { + "label": "ApartmentL", + "value": "725" + }, + { + "label": "Assassin", + "value": "1055" + }, + { + "label": "Alchemist", + "value": "1000" + }, + { + "label": "artillery", + "value": "5124" + }, + { + "label": "Anti-HeroL", + "value": "782" + }, + { + "label": "Anti-Magic", + "value": "688" + }, + { + "label": "Automatons", + "value": "675" + }, + { + "label": "Abandoned", + "value": "5452" + }, + { + "label": "Aishara", + "value": "4885" + }, + { + "label": "AngryChick", + "value": "4426" + }, + { + "label": "archeology", + "value": "3629" + }, + { + "label": "Abilities", + "value": "1691" + }, + { + "label": "AutomaticU", + "value": "1639" + }, + { + "label": "Ancient", + "value": "1546" + }, + { + "label": "Actors", + "value": "1101" + }, + { + "label": "AdaptedtoV", + "value": "973" + }, + { + "label": "AmusementP", + "value": "766" + }, + { + "label": "Astrologer", + "value": "700" + }, + { + "label": "Amatchmade", + "value": "6129" + }, + { + "label": "AmadaR", + "value": "5792" + }, + { + "label": "An83", + "value": "5020" + }, + { + "label": "AkiraHayak", + "value": "4595" + }, + { + "label": "Abyss", + "value": "3971" + }, + { + "label": "AhQing", + "value": "3965" + }, + { + "label": "Alicia", + "value": "3758" + }, + { + "label": "Amon&03", + "value": "3740" + }, + { + "label": "Arbor11", + "value": "3673" + }, + { + "label": "AmericanCo", + "value": "2899" + }, + { + "label": "Age-gap", + "value": "1943" + }, + { + "label": "atravellin", + "value": "1832" + }, + { + "label": "animals", + "value": "1694" + }, + { + "label": "Actress", + "value": "1526" + }, + { + "label": "Adrogynous", + "value": "1105" + }, + { + "label": "All-GirlsS", + "value": "1024" + }, + { + "label": "Angst", + "value": "1007" + }, + { + "label": "Award-winn", + "value": "985" + }, + { + "label": "AbusiveCha", + "value": "815" + }, + { + "label": "AI", + "value": "788" + }, + { + "label": "AnimalChar", + "value": "120" + }, + { + "label": "Adaptedfro", + "value": "6203" + }, + { + "label": "Aesthetic", + "value": "6163" + }, + { + "label": "Ancientmar", + "value": "6150" + }, + { + "label": "Angel&0", + "value": "6127" + }, + { + "label": "AnNuan84", + "value": "6085" + }, + { + "label": "a55421", + "value": "6070" + }, + { + "label": "amateurtra", + "value": "6059" + }, + { + "label": "Acane", + "value": "5993" + }, + { + "label": "Amnesty", + "value": "5965" + }, + { + "label": "azure", + "value": "5954" + }, + { + "label": "ArthurJun", + "value": "5940" + }, + { + "label": "Apassingfo", + "value": "5913" + }, + { + "label": "Acorneroft", + "value": "5910" + }, + { + "label": "Aimingatth", + "value": "5897" + }, + { + "label": "AJiu", + "value": "5892" + }, + { + "label": "AladleofCh", + "value": "5873" + }, + { + "label": "Apotofrefr", + "value": "5865" + }, + { + "label": "ApostleCel", + "value": "5858" + }, + { + "label": "afigure", + "value": "5843" + }, + { + "label": "AmanChuanl", + "value": "5841" + }, + { + "label": "Asmalloffi", + "value": "5766" + }, + { + "label": "ATom", + "value": "5763" + }, + { + "label": "ajugofsake", + "value": "5736" + }, + { + "label": "AoChen", + "value": "5731" + }, + { + "label": "Awakeningf", + "value": "5715" + }, + { + "label": "Aoyamafell", + "value": "5662" + }, + { + "label": "Administra", + "value": "5659" + }, + { + "label": "asaltedfis", + "value": "5638" + }, + { + "label": "Alpaca", + "value": "5616" + }, + { + "label": "assassin95", + "value": "5553" + }, + { + "label": "Ahbig", + "value": "5530" + }, + { + "label": "Aston", + "value": "5519" + }, + { + "label": "awhitebait", + "value": "5517" + }, + { + "label": "Ascension", + "value": "5497" + }, + { + "label": "AiAiwhowor", + "value": "5491" + }, + { + "label": "astonelion", + "value": "5477" + }, + { + "label": "Apocalypse", + "value": "5466" + }, + { + "label": "autumnisno", + "value": "5436" + }, + { + "label": "Anonymouso", + "value": "5420" + }, + { + "label": "anglemetal", + "value": "5414" + }, + { + "label": "anovernigh", + "value": "5407" + }, + { + "label": "AltecNewco", + "value": "5298" + }, + { + "label": "alazybones", + "value": "5295" + }, + { + "label": "Amis", + "value": "5279" + }, + { + "label": "Ayres", + "value": "5241" + }, + { + "label": "Anonymous", + "value": "5229" + }, + { + "label": "asword", + "value": "5103" + }, + { + "label": "ADeadBody", + "value": "5069" + }, + { + "label": "apigeon", + "value": "5029" + }, + { + "label": "Amelonisno", + "value": "5026" + }, + { + "label": "anamewithd", + "value": "5019" + }, + { + "label": "AbaloneHou", + "value": "5017" + }, + { + "label": "Asasi", + "value": "4951" + }, + { + "label": "ailii", + "value": "4923" + }, + { + "label": "AlittleJun", + "value": "4921" + }, + { + "label": "apassingfi", + "value": "4891" + }, + { + "label": "agluttonou", + "value": "4884" + }, + { + "label": "amoonrabbi", + "value": "4877" + }, + { + "label": "A.I", + "value": "4862" + }, + { + "label": "ambitionin", + "value": "4853" + }, + { + "label": "AnonymousX", + "value": "4816" + }, + { + "label": "Acupoffrag", + "value": "4815" + }, + { + "label": "AozakiAoko", + "value": "4811" + }, + { + "label": "AskJianZho", + "value": "4807" + }, + { + "label": "afinegray", + "value": "4804" + }, + { + "label": "Asukatired", + "value": "4803" + }, + { + "label": "alsothough", + "value": "4775" + }, + { + "label": "acanofcoke", + "value": "4689" + }, + { + "label": "Admiral-sa", + "value": "4680" + }, + { + "label": "AngelofGai", + "value": "4636" + }, + { + "label": "aswordbone", + "value": "4617" + }, + { + "label": "apapertige", + "value": "4603" + }, + { + "label": "adreamcome", + "value": "4569" + }, + { + "label": "AliceSupre", + "value": "4557" + }, + { + "label": "alittlebit", + "value": "4546" + }, + { + "label": "akitten", + "value": "4536" + }, + { + "label": "AutumnLeav", + "value": "4518" + }, + { + "label": "ashtrayold", + "value": "4499" + }, + { + "label": "afterthera", + "value": "4489" + }, + { + "label": "Agleamoffl", + "value": "4483" + }, + { + "label": "AuthorJi", + "value": "4443" + }, + { + "label": "Abowlofduk", + "value": "4398" + }, + { + "label": "atabbycat", + "value": "4395" + }, + { + "label": "anorangetr", + "value": "4389" + }, + { + "label": "andfall", + "value": "4373" + }, + { + "label": "Adventurer", + "value": "4342" + }, + { + "label": "alpha", + "value": "4340" + }, + { + "label": "Alldaylong", + "value": "4316" + }, + { + "label": "Almostsir", + "value": "4238" + }, + { + "label": "alittlewol", + "value": "4232" + }, + { + "label": "Anon", + "value": "4223" + }, + { + "label": "aheart", + "value": "4219" + }, + { + "label": "aturkey", + "value": "4203" + }, + { + "label": "ABowlofCar", + "value": "4168" + }, + { + "label": "autumncold", + "value": "4142" + }, + { + "label": "anotherpac", + "value": "4138" + }, + { + "label": "Ashcanfly", + "value": "4110" + }, + { + "label": "alazyscale", + "value": "4096" + }, + { + "label": "angeldoom", + "value": "4089" + }, + { + "label": "amberstar", + "value": "4087" + }, + { + "label": "AngelPeerl", + "value": "4059" + }, + { + "label": "Aquadoesn&", + "value": "4050" + }, + { + "label": "aglassofhu", + "value": "4049" + }, + { + "label": "acloudpier", + "value": "4002" + }, + { + "label": "Amouthfulo", + "value": "3980" + }, + { + "label": "adventurep", + "value": "3942" + }, + { + "label": "Alloverthe", + "value": "3897" + }, + { + "label": "Anarchism", + "value": "3855" + }, + { + "label": "anti-entro", + "value": "3820" + }, + { + "label": "Ashesthatd", + "value": "3803" + }, + { + "label": "anoldliqun", + "value": "3792" + }, + { + "label": "Anas", + "value": "3764" + }, + { + "label": "acupofwarm", + "value": "3745" + }, + { + "label": "autumnleav", + "value": "3685" + }, + { + "label": "Absorbform", + "value": "3681" + }, + { + "label": "allergytoe", + "value": "3665" + }, + { + "label": "aquablue", + "value": "3642" + }, + { + "label": "armoredcit", + "value": "3558" + }, + { + "label": "apieceofno", + "value": "3498" + }, + { + "label": "asmallgras", + "value": "3497" + }, + { + "label": "AThousandM", + "value": "3484" + }, + { + "label": "ahundredth", + "value": "3475" + }, + { + "label": "Assasins", + "value": "3462" + }, + { + "label": "Americaiss", + "value": "3450" + }, + { + "label": "ASOIAF", + "value": "3423" + }, + { + "label": "adventerer", + "value": "3416" + }, + { + "label": "Anti-routi", + "value": "3073" + }, + { + "label": "airflowgen", + "value": "3051" + }, + { + "label": "AlternateH", + "value": "3026" + }, + { + "label": "AlternateH", + "value": "2995" + }, + { + "label": "AlternateH", + "value": "2948" + }, + { + "label": "Artificial", + "value": "2919" + }, + { + "label": "Anti-MC", + "value": "2916" + }, + { + "label": "ArmsDealer", + "value": "2907" + }, + { + "label": "Americas", + "value": "2903" + }, + { + "label": "AlterateHi", + "value": "2902" + }, + { + "label": "AncientChi", + "value": "2892" + }, + { + "label": "AnlanInvin", + "value": "2842" + }, + { + "label": "Almightypl", + "value": "2780" + }, + { + "label": "AlmightyCo", + "value": "2778" + }, + { + "label": "Aliverday", + "value": "2656" + }, + { + "label": "AZanpakut", + "value": "2637" + }, + { + "label": "Alone", + "value": "2610" + }, + { + "label": "Anautumnra", + "value": "2577" + }, + { + "label": "avigorous", + "value": "2466" + }, + { + "label": "Amagicpill", + "value": "2434" + }, + { + "label": "AllHeavens", + "value": "2348" + }, + { + "label": "angryhouse", + "value": "2331" + }, + { + "label": "askTaichi", + "value": "2316" + }, + { + "label": "Auspicious", + "value": "2298" + }, + { + "label": "anoldman", + "value": "2268" + }, + { + "label": "absolutely", + "value": "2255" + }, + { + "label": "animenewco", + "value": "2208" + }, + { + "label": "arayofsuns", + "value": "2126" + }, + { + "label": "Ayanokoji", + "value": "2066" + }, + { + "label": "Aaron&0", + "value": "2064" + }, + { + "label": "Authoroffa", + "value": "2018" + }, + { + "label": "allenzhang", + "value": "2015" + }, + { + "label": "Apple", + "value": "1962" + }, + { + "label": "AgeofGods", + "value": "1897" + }, + { + "label": "AfricanEmi", + "value": "1894" + }, + { + "label": "agrass", + "value": "1893" + }, + { + "label": "ahveryfish", + "value": "1876" + }, + { + "label": "autumnautu", + "value": "1838" + }, + { + "label": "Aftertheso", + "value": "1818" + }, + { + "label": "assasin", + "value": "1763" + }, + { + "label": "artificer", + "value": "1755" + }, + { + "label": "AncientWea", + "value": "1730" + }, + { + "label": "Animator", + "value": "1704" + }, + { + "label": "Adoption", + "value": "1697" + }, + { + "label": "Abortion", + "value": "1696" + }, + { + "label": "AbsoluteDu", + "value": "1670" + }, + { + "label": "ACGN", + "value": "1653" + }, + { + "label": "Attractive", + "value": "1648" + }, + { + "label": "Avatar&", + "value": "1635" + }, + { + "label": "Aristrocac", + "value": "1598" + }, + { + "label": "AbandonedC", + "value": "1597" + }, + { + "label": "Angel", + "value": "1589" + }, + { + "label": "AdvancedKn", + "value": "1564" + }, + { + "label": "AnotherWor", + "value": "1550" + }, + { + "label": "Arknights", + "value": "1540" + }, + { + "label": "AcceptingD", + "value": "1507" + }, + { + "label": "Apocalypti", + "value": "1485" + }, + { + "label": "AncientRea", + "value": "1478" + }, + { + "label": "Aggressive", + "value": "1476" + }, + { + "label": "ancientset", + "value": "1465" + }, + { + "label": "anthology", + "value": "1440" + }, + { + "label": "anewworld", + "value": "1425" + }, + { + "label": "ArmsTrade", + "value": "1389" + }, + { + "label": "Apprentice", + "value": "1376" + }, + { + "label": "AutomaticU", + "value": "1312" + }, + { + "label": "Adopted", + "value": "1300" + }, + { + "label": "AncientBus", + "value": "1257" + }, + { + "label": "Apocalypse", + "value": "1233" + }, + { + "label": "AI-chip", + "value": "1181" + }, + { + "label": "Appraisal", + "value": "1180" + }, + { + "label": "Anti-Hero", + "value": "1174" + }, + { + "label": "Anti-heroP", + "value": "1171" + }, + { + "label": "Apocalypse", + "value": "1081" + }, + { + "label": "AzurLane", + "value": "1068" + }, + { + "label": "ancienttim", + "value": "1049" + }, + { + "label": "AmoralityP", + "value": "1044" + }, + { + "label": "AkamegaKil", + "value": "1040" + }, + { + "label": "AgeDiffere", + "value": "939" + }, + { + "label": "AverageLoo", + "value": "905" + }, + { + "label": "Artis", + "value": "869" + }, + { + "label": "AcasualPaw", + "value": "806" + }, + { + "label": "Artist", + "value": "760" + }, + { + "label": "ArtifactsB", + "value": "567" + }, + { + "label": "Anti-socia", + "value": "454" + }, + { + "label": "AncientChi", + "value": "424" + }, + { + "label": "Androgynou", + "value": "318" + }, + { + "label": "Average-lo", + "value": "245" + }, + { + "label": "Appearance", + "value": "104" + }, + { + "label": "Artificial", + "value": "91" + }, + { + "label": "BeautifulF", + "value": "21" + }, + { + "label": "Bellyblack", + "value": "3117" + }, + { + "label": "bigbrain", + "value": "3125" + }, + { + "label": "BusinessMa", + "value": "60" + }, + { + "label": "BlackBelly", + "value": "38" + }, + { + "label": "Betrayal", + "value": "40" + }, + { + "label": "businessfl", + "value": "5679" + }, + { + "label": "BeastCompa", + "value": "57" + }, + { + "label": "Beasts", + "value": "58" + }, + { + "label": "BodyTemper", + "value": "246" + }, + { + "label": "Bloodlines", + "value": "9" + }, + { + "label": "Businessme", + "value": "227" + }, + { + "label": "Basketball", + "value": "226" + }, + { + "label": "Blackening", + "value": "3219" + }, + { + "label": "BehindtheS", + "value": "1643" + }, + { + "label": "BickeringC", + "value": "79" + }, + { + "label": "BattleComp", + "value": "211" + }, + { + "label": "BrokenEnga", + "value": "231" + }, + { + "label": "Buddhism", + "value": "442" + }, + { + "label": "Blacklotus", + "value": "3366" + }, + { + "label": "Beastkin", + "value": "8" + }, + { + "label": "Bullying", + "value": "134" + }, + { + "label": "BattleAcad", + "value": "416" + }, + { + "label": "beastworld", + "value": "3441" + }, + { + "label": "Blacktechn", + "value": "5700" + }, + { + "label": "Brotherhoo", + "value": "570" + }, + { + "label": "Blacksmith", + "value": "379" + }, + { + "label": "BrotherCom", + "value": "521" + }, + { + "label": "Bodyguards", + "value": "439" + }, + { + "label": "Books", + "value": "337" + }, + { + "label": "BOSSflow", + "value": "3324" + }, + { + "label": "Bleach", + "value": "1123" + }, + { + "label": "Blackmail", + "value": "171" + }, + { + "label": "Blinddate", + "value": "3694" + }, + { + "label": "becomeagod", + "value": "3215" + }, + { + "label": "BodySwap", + "value": "269" + }, + { + "label": "bodyguard", + "value": "3701" + }, + { + "label": "BDSM", + "value": "190" + }, + { + "label": "Boss", + "value": "2911" + }, + { + "label": "Brother", + "value": "3609" + }, + { + "label": "beautifulh", + "value": "59" + }, + { + "label": "BasedonaVi", + "value": "1014" + }, + { + "label": "Business", + "value": "663" + }, + { + "label": "Bookworm", + "value": "659" + }, + { + "label": "BasedonaMo", + "value": "579" + }, + { + "label": "Beasttamer", + "value": "1254" + }, + { + "label": "BlindProta", + "value": "481" + }, + { + "label": "Biochip", + "value": "473" + }, + { + "label": "BasedonanA", + "value": "1052" + }, + { + "label": "BloodManip", + "value": "534" + }, + { + "label": "Brainwashi", + "value": "396" + }, + { + "label": "BasedonaTV", + "value": "671" + }, + { + "label": "baby", + "value": "3594" + }, + { + "label": "Bestiality", + "value": "628" + }, + { + "label": "Bulldozer", + "value": "1614" + }, + { + "label": "Butlers", + "value": "763" + }, + { + "label": "BlindDates", + "value": "676" + }, + { + "label": "Beasttamin", + "value": "5057" + }, + { + "label": "BusinessMa", + "value": "1069" + }, + { + "label": "BeautifulC", + "value": "1018" + }, + { + "label": "BreastFeti", + "value": "974" + }, + { + "label": "BisexualPr", + "value": "813" + }, + { + "label": "BigBroHasD", + "value": "802" + }, + { + "label": "bluestone", + "value": "2206" + }, + { + "label": "blooddemon", + "value": "2095" + }, + { + "label": "Beautifull", + "value": "1516" + }, + { + "label": "book", + "value": "1415" + }, + { + "label": "Beast", + "value": "1253" + }, + { + "label": "BeautifulP", + "value": "1160" + }, + { + "label": "BL", + "value": "857" + }, + { + "label": "Baseball", + "value": "673" + }, + { + "label": "Boxing", + "value": "667" + }, + { + "label": "beardada", + "value": "5798" + }, + { + "label": "bigcatpres", + "value": "5782" + }, + { + "label": "Beat", + "value": "5170" + }, + { + "label": "Bloodpumpi", + "value": "5062" + }, + { + "label": "Behappytod", + "value": "4836" + }, + { + "label": "bellsofjoy", + "value": "4568" + }, + { + "label": "beardwhite", + "value": "4253" + }, + { + "label": "blackteais", + "value": "3866" + }, + { + "label": "burnout", + "value": "2694" + }, + { + "label": "BraisedPai", + "value": "2132" + }, + { + "label": "BuLofan", + "value": "2068" + }, + { + "label": "BungouStra", + "value": "1773" + }, + { + "label": "buildingki", + "value": "1764" + }, + { + "label": "Billionair", + "value": "1457" + }, + { + "label": "beauty", + "value": "1418" + }, + { + "label": "Beastmen", + "value": "1369" + }, + { + "label": "Blind", + "value": "1234" + }, + { + "label": "Bloodline", + "value": "1205" + }, + { + "label": "beastcompa", + "value": "1128" + }, + { + "label": "BasedonaVi", + "value": "1083" + }, + { + "label": "Body-doubl", + "value": "980" + }, + { + "label": "Bully", + "value": "786" + }, + { + "label": "Beautiful", + "value": "6200" + }, + { + "label": "Bgfellow", + "value": "6162" + }, + { + "label": "bite", + "value": "6125" + }, + { + "label": "BeidouTian", + "value": "6050" + }, + { + "label": "bigorange", + "value": "6046" + }, + { + "label": "bellbell", + "value": "6045" + }, + { + "label": "bluenight", + "value": "6036" + }, + { + "label": "BucketofIn", + "value": "6016" + }, + { + "label": "Bishamonte", + "value": "5992" + }, + { + "label": "Bagpipeson", + "value": "5977" + }, + { + "label": "bluemoonco", + "value": "5966" + }, + { + "label": "Brewingflo", + "value": "5909" + }, + { + "label": "Beginner", + "value": "5878" + }, + { + "label": "BrigadeSec", + "value": "5872" + }, + { + "label": "Beautifulb", + "value": "5870" + }, + { + "label": "BrokenDanf", + "value": "5840" + }, + { + "label": "Bombardmen", + "value": "5830" + }, + { + "label": "brokenfrui", + "value": "5823" + }, + { + "label": "Bambooskin", + "value": "5776" + }, + { + "label": "BlazingAng", + "value": "5739" + }, + { + "label": "bigblue", + "value": "5660" + }, + { + "label": "BashuiDuxi", + "value": "5653" + }, + { + "label": "bigfire", + "value": "5651" + }, + { + "label": "bambooshad", + "value": "5643" + }, + { + "label": "BaldTraine", + "value": "5628" + }, + { + "label": "BoundlessL", + "value": "5622" + }, + { + "label": "Bubble012", + "value": "5590" + }, + { + "label": "BrainJam", + "value": "5588" + }, + { + "label": "blueskyspa", + "value": "5565" + }, + { + "label": "brokendrea", + "value": "5543" + }, + { + "label": "BrokenYuri", + "value": "5525" + }, + { + "label": "北八", + "value": "5520" + }, + { + "label": "blackcatam", + "value": "5509" + }, + { + "label": "BoboXiaolu", + "value": "5479" + }, + { + "label": "BaiMaoisno", + "value": "5472" + }, + { + "label": "Betweenthe", + "value": "5456" + }, + { + "label": "BaiBailan~", + "value": "5427" + }, + { + "label": "BeiHaiPiao", + "value": "5421" + }, + { + "label": "BrotherQin", + "value": "5395" + }, + { + "label": "Baumudrich", + "value": "5375" + }, + { + "label": "BossSim", + "value": "5366" + }, + { + "label": "building", + "value": "5347" + }, + { + "label": "Breeze&", + "value": "5290" + }, + { + "label": "bigchinchi", + "value": "5274" + }, + { + "label": "Baldmaster", + "value": "5207" + }, + { + "label": "blackgreen", + "value": "5203" + }, + { + "label": "BalmoreBea", + "value": "5189" + }, + { + "label": "Betweenthe", + "value": "5172" + }, + { + "label": "bigfacemor", + "value": "5146" + }, + { + "label": "BeishanRai", + "value": "5112" + }, + { + "label": "Beatthefem", + "value": "5065" + }, + { + "label": "Beatthemal", + "value": "5064" + }, + { + "label": "bickeringl", + "value": "5054" + }, + { + "label": "Bowandjack", + "value": "5041" + }, + { + "label": "Butterrice", + "value": "5038" + }, + { + "label": "bloodytuna", + "value": "5037" + }, + { + "label": "Browlin", + "value": "5022" + }, + { + "label": "broken", + "value": "5004" + }, + { + "label": "BishopMyri", + "value": "5001" + }, + { + "label": "BaiTuanJiu", + "value": "4995" + }, + { + "label": "blackflash", + "value": "4984" + }, + { + "label": "BZ", + "value": "4982" + }, + { + "label": "bulgingbel", + "value": "4965" + }, + { + "label": "bigpeninth", + "value": "4948" + }, + { + "label": "Bunsaresol", + "value": "4936" + }, + { + "label": "bambooink", + "value": "4918" + }, + { + "label": "BatDemonCh", + "value": "4912" + }, + { + "label": "BaiMingxia", + "value": "4875" + }, + { + "label": "braisedfis", + "value": "4839" + }, + { + "label": "bluepigeon", + "value": "4837" + }, + { + "label": "bubblebath", + "value": "4802" + }, + { + "label": "bigcatslav", + "value": "4793" + }, + { + "label": "BeefandShr", + "value": "4790" + }, + { + "label": "BurningEye", + "value": "4773" + }, + { + "label": "BBQmadebyt", + "value": "4765" + }, + { + "label": "BladeofFal", + "value": "4756" + }, + { + "label": "BurningEye", + "value": "4711" + }, + { + "label": "blackreinc", + "value": "4709" + }, + { + "label": "brokenred", + "value": "4677" + }, + { + "label": "Benson", + "value": "4668" + }, + { + "label": "brokengodo", + "value": "4634" + }, + { + "label": "boxser", + "value": "4631" + }, + { + "label": "brokendrea", + "value": "4611" + }, + { + "label": "blowdreamt", + "value": "4598" + }, + { + "label": "blacksword", + "value": "4593" + }, + { + "label": "badcheese", + "value": "4584" + }, + { + "label": "brokenlitt", + "value": "4580" + }, + { + "label": "Blackbirch", + "value": "4572" + }, + { + "label": "Beautyred", + "value": "4560" + }, + { + "label": "blindnomik", + "value": "4532" + }, + { + "label": "Brainisuna", + "value": "4509" + }, + { + "label": "BambooFlow", + "value": "4506" + }, + { + "label": "bigisbeaut", + "value": "4498" + }, + { + "label": "BlueSkyDem", + "value": "4495" + }, + { + "label": "Botharetwo", + "value": "4486" + }, + { + "label": "backtoboat", + "value": "4479" + }, + { + "label": "bluelight", + "value": "4455" + }, + { + "label": "BaiYuenake", + "value": "4432" + }, + { + "label": "brokenhear", + "value": "4397" + }, + { + "label": "BrotherInL", + "value": "4346" + }, + { + "label": "bluewhale", + "value": "4333" + }, + { + "label": "billionpoi", + "value": "4330" + }, + { + "label": "brokenmoon", + "value": "4317" + }, + { + "label": "blacktea", + "value": "4303" + }, + { + "label": "BabyFeng", + "value": "4278" + }, + { + "label": "bluewaterg", + "value": "4269" + }, + { + "label": "Buddhistco", + "value": "4220" + }, + { + "label": "brokenambi", + "value": "4153" + }, + { + "label": "blindzhang", + "value": "4139" + }, + { + "label": "BronzeEndo", + "value": "4135" + }, + { + "label": "blackabyss", + "value": "4081" + }, + { + "label": "blacksilkj", + "value": "4014" + }, + { + "label": "Bunnyeatse", + "value": "3974" + }, + { + "label": "blackpupil", + "value": "3973" + }, + { + "label": "blast", + "value": "3966" + }, + { + "label": "BeichengNa", + "value": "3945" + }, + { + "label": "blackpupil", + "value": "3944" + }, + { + "label": "Booksonthe", + "value": "3907" + }, + { + "label": "Bowandshoo", + "value": "3902" + }, + { + "label": "BrutalBeas", + "value": "3884" + }, + { + "label": "半弦", + "value": "3873" + }, + { + "label": "babbling1", + "value": "3862" + }, + { + "label": "bigdreamsa", + "value": "3752" + }, + { + "label": "BigSesameR", + "value": "3721" + }, + { + "label": "Bodhi", + "value": "3661" + }, + { + "label": "bamboobamb", + "value": "3654" + }, + { + "label": "bouncingei", + "value": "3582" + }, + { + "label": "BraisedPis", + "value": "3541" + }, + { + "label": "blackInter", + "value": "3506" + }, + { + "label": "BearChild", + "value": "3461" + }, + { + "label": "BuddhaofNi", + "value": "3376" + }, + { + "label": "blackice", + "value": "3375" + }, + { + "label": "Bandit", + "value": "3302" + }, + { + "label": "basketball", + "value": "3068" + }, + { + "label": "becomeagod", + "value": "3022" + }, + { + "label": "bookslikeu", + "value": "2922" + }, + { + "label": "broalwaysg", + "value": "2870" + }, + { + "label": "billionpeo", + "value": "2868" + }, + { + "label": "bluesilk", + "value": "2832" + }, + { + "label": "balduncle", + "value": "2830" + }, + { + "label": "bewitching", + "value": "2828" + }, + { + "label": "BaiXiaowei", + "value": "2820" + }, + { + "label": "blackandwh", + "value": "2797" + }, + { + "label": "Boiled", + "value": "2793" + }, + { + "label": "Beansandgr", + "value": "2786" + }, + { + "label": "BlackDrago", + "value": "2720" + }, + { + "label": "blackcatis", + "value": "2711" + }, + { + "label": "beggingfor", + "value": "2710" + }, + { + "label": "Burningmou", + "value": "2703" + }, + { + "label": "beautifula", + "value": "2691" + }, + { + "label": "bighippo", + "value": "2683" + }, + { + "label": "bluesilksu", + "value": "2675" + }, + { + "label": "bearcocoa", + "value": "2652" + }, + { + "label": "becausesoh", + "value": "2649" + }, + { + "label": "Bringaknif", + "value": "2643" + }, + { + "label": "boycold", + "value": "2626" + }, + { + "label": "bigtent", + "value": "2605" + }, + { + "label": "Bigcockcut", + "value": "2586" + }, + { + "label": "Buildthewo", + "value": "2571" + }, + { + "label": "Breakingth", + "value": "2565" + }, + { + "label": "beaming", + "value": "2508" + }, + { + "label": "Bodhicitta", + "value": "2507" + }, + { + "label": "BookDustSp", + "value": "2416" + }, + { + "label": "Belltouche", + "value": "2404" + }, + { + "label": "baldman", + "value": "2395" + }, + { + "label": "BoXiaowen", + "value": "2389" + }, + { + "label": "bigcitysma", + "value": "2367" + }, + { + "label": "bigorangew", + "value": "2319" + }, + { + "label": "beastprota", + "value": "2315" + }, + { + "label": "baldnessat", + "value": "2313" + }, + { + "label": "breaktheke", + "value": "2263" + }, + { + "label": "BarrenEmpe", + "value": "2260" + }, + { + "label": "BloodMoonG", + "value": "2237" + }, + { + "label": "BigSkeleto", + "value": "2235" + }, + { + "label": "Bearchildl", + "value": "2232" + }, + { + "label": "BrotherZhu", + "value": "2227" + }, + { + "label": "blackandim", + "value": "2219" + }, + { + "label": "bitefire", + "value": "2212" + }, + { + "label": "beatyourse", + "value": "2204" + }, + { + "label": "blueshirts", + "value": "2192" + }, + { + "label": "BrotherChe", + "value": "2181" + }, + { + "label": "blacksoil", + "value": "2144" + }, + { + "label": "Bearcat", + "value": "2142" + }, + { + "label": "BingtangHu", + "value": "2140" + }, + { + "label": "BookstoreS", + "value": "2133" + }, + { + "label": "Breeze", + "value": "2101" + }, + { + "label": "Brightmoon", + "value": "2073" + }, + { + "label": "breezesilv", + "value": "2045" + }, + { + "label": "Bigdog", + "value": "2042" + }, + { + "label": "belovedbab", + "value": "1992" + }, + { + "label": "Boundlessf", + "value": "1982" + }, + { + "label": "ButterflyD", + "value": "1980" + }, + { + "label": "bloomingon", + "value": "1951" + }, + { + "label": "Becomefamo", + "value": "1921" + }, + { + "label": "bigwhitewh", + "value": "1911" + }, + { + "label": "Bigplayers", + "value": "1880" + }, + { + "label": "blackandwh", + "value": "1867" + }, + { + "label": "Brownsugar", + "value": "1866" + }, + { + "label": "Biscuits", + "value": "1846" + }, + { + "label": "BloodofAni", + "value": "1822" + }, + { + "label": "BusinessRi", + "value": "1814" + }, + { + "label": "bigpicture", + "value": "1796" + }, + { + "label": "BeautifulF", + "value": "1748" + }, + { + "label": "beautifulf", + "value": "1746" + }, + { + "label": "Blacklight", + "value": "1707" + }, + { + "label": "Biomass", + "value": "1706" + }, + { + "label": "beastman", + "value": "1700" + }, + { + "label": "Badassprot", + "value": "1695" + }, + { + "label": "Basket", + "value": "1606" + }, + { + "label": "Blackbelli", + "value": "1556" + }, + { + "label": "Bloodborne", + "value": "1541" + }, + { + "label": "BuildKingd", + "value": "1539" + }, + { + "label": "Bussinesma", + "value": "1514" + }, + { + "label": "BehindtheS", + "value": "1505" + }, + { + "label": "BritishEmp", + "value": "1489" + }, + { + "label": "Beautifulf", + "value": "1456" + }, + { + "label": "Bussiness", + "value": "1450" + }, + { + "label": "breakingli", + "value": "1441" + }, + { + "label": "ben10", + "value": "1412" + }, + { + "label": "BunguoStra", + "value": "1381" + }, + { + "label": "Breakup", + "value": "1344" + }, + { + "label": "BattleThro", + "value": "1333" + }, + { + "label": "BTTH", + "value": "1288" + }, + { + "label": "Black-bell", + "value": "1273" + }, + { + "label": "Babies", + "value": "1223" + }, + { + "label": "BookWearer", + "value": "1153" + }, + { + "label": "BasedonaSo", + "value": "1118" + }, + { + "label": "BlackBelly", + "value": "1053" + }, + { + "label": "bottommc", + "value": "1030" + }, + { + "label": "BusinessDe", + "value": "962" + }, + { + "label": "businessor", + "value": "954" + }, + { + "label": "BookTransm", + "value": "947" + }, + { + "label": "Black-bell", + "value": "925" + }, + { + "label": "BusinessEm", + "value": "761" + }, + { + "label": "Boss-Subor", + "value": "80" + }, + { + "label": "Cross", + "value": "3090" + }, + { + "label": "Chinese", + "value": "1675" + }, + { + "label": "Cultivatio", + "value": "63" + }, + { + "label": "Counteratt", + "value": "1619" + }, + { + "label": "clearthink", + "value": "3237" + }, + { + "label": "CalmProtag", + "value": "22" + }, + { + "label": "CleverProt", + "value": "92" + }, + { + "label": "calm", + "value": "3164" + }, + { + "label": "Celebrity", + "value": "880" + }, + { + "label": "Campus", + "value": "1677" + }, + { + "label": "Chronology", + "value": "1451" + }, + { + "label": "comprehens", + "value": "3190" + }, + { + "label": "Cheats", + "value": "62" + }, + { + "label": "CunningPro", + "value": "164" + }, + { + "label": "Celebritie", + "value": "23" + }, + { + "label": "Comprehens", + "value": "3119" + }, + { + "label": "Cute", + "value": "1658" + }, + { + "label": "ComedicUnd", + "value": "154" + }, + { + "label": "CharacterG", + "value": "61" + }, + { + "label": "Childcare", + "value": "81" + }, + { + "label": "ColdLoveIn", + "value": "84" + }, + { + "label": "ColdProtag", + "value": "10" + }, + { + "label": "ChineseNov", + "value": "1679" + }, + { + "label": "Cohabitati", + "value": "152" + }, + { + "label": "CaringProt", + "value": "122" + }, + { + "label": "cure", + "value": "3251" + }, + { + "label": "Contendfor", + "value": "3093" + }, + { + "label": "CuteProtag", + "value": "124" + }, + { + "label": "Cooking", + "value": "161" + }, + { + "label": "ConfidentP", + "value": "232" + }, + { + "label": "cautious", + "value": "3193" + }, + { + "label": "CautiousPr", + "value": "222" + }, + { + "label": "chasingwif", + "value": "3330" + }, + { + "label": "concubine", + "value": "3255" + }, + { + "label": "CollegeStr", + "value": "3095" + }, + { + "label": "CuteChildr", + "value": "224" + }, + { + "label": "CarefreePr", + "value": "121" + }, + { + "label": "contractma", + "value": "3134" + }, + { + "label": "ClassicXia", + "value": "3112" + }, + { + "label": "CoupleGrow", + "value": "153" + }, + { + "label": "comedy", + "value": "878" + }, + { + "label": "CruelChara", + "value": "42" + }, + { + "label": "ChildhoodF", + "value": "197" + }, + { + "label": "CharmingPr", + "value": "520" + }, + { + "label": "ChatGroup", + "value": "890" + }, + { + "label": "CuteStory", + "value": "125" + }, + { + "label": "CollegeUni", + "value": "169" + }, + { + "label": "Cross-dres", + "value": "155" + }, + { + "label": "chief", + "value": "3699" + }, + { + "label": "ChildProta", + "value": "491" + }, + { + "label": "Cheat", + "value": "888" + }, + { + "label": "clear", + "value": "3338" + }, + { + "label": "Chaotang", + "value": "3123" + }, + { + "label": "Crafting", + "value": "374" + }, + { + "label": "ClingyLove", + "value": "297" + }, + { + "label": "Card", + "value": "1379" + }, + { + "label": "cannonfodd", + "value": "3350" + }, + { + "label": "Cthulhu", + "value": "1542" + }, + { + "label": "ChaotangJi", + "value": "3282" + }, + { + "label": "Crime", + "value": "440" + }, + { + "label": "ChildhoodL", + "value": "176" + }, + { + "label": "cynicism", + "value": "3456" + }, + { + "label": "ClanBuildi", + "value": "327" + }, + { + "label": "ChinesePre", + "value": "1303" + }, + { + "label": "contractlo", + "value": "3600" + }, + { + "label": "Contest", + "value": "5699" + }, + { + "label": "chase", + "value": "3597" + }, + { + "label": "CardGames", + "value": "512" + }, + { + "label": "CosmicWars", + "value": "338" + }, + { + "label": "Crossdress", + "value": "1087" + }, + { + "label": "ChatRooms", + "value": "142" + }, + { + "label": "Contrastcu", + "value": "3455" + }, + { + "label": "ChildAbuse", + "value": "393" + }, + { + "label": "Contracts", + "value": "294" + }, + { + "label": "chanceenco", + "value": "3631" + }, + { + "label": "cook", + "value": "3317" + }, + { + "label": "ClanSectDe", + "value": "1316" + }, + { + "label": "Chinesemed", + "value": "3166" + }, + { + "label": "coach", + "value": "3553" + }, + { + "label": "CP", + "value": "3331" + }, + { + "label": "Cannibalis", + "value": "407" + }, + { + "label": "Chefs", + "value": "260" + }, + { + "label": "ClumsyLove", + "value": "204" + }, + { + "label": "Couple", + "value": "3309" + }, + { + "label": "Clones", + "value": "644" + }, + { + "label": "Comprehens", + "value": "6172" + }, + { + "label": "collapse", + "value": "3297" + }, + { + "label": "Crush", + "value": "5674" + }, + { + "label": "CharacterD", + "value": "341" + }, + { + "label": "ChildishPr", + "value": "123" + }, + { + "label": "Corruption", + "value": "408" + }, + { + "label": "CourtOffic", + "value": "594" + }, + { + "label": "ChildhoodS", + "value": "590" + }, + { + "label": "Chef", + "value": "829" + }, + { + "label": "Curses", + "value": "528" + }, + { + "label": "CowardlyPr", + "value": "290" + }, + { + "label": "Criminals", + "value": "173" + }, + { + "label": "Chronicles", + "value": "6169" + }, + { + "label": "coolandhan", + "value": "3735" + }, + { + "label": "Civilizati", + "value": "3329" + }, + { + "label": "commander", + "value": "3200" + }, + { + "label": "Crossover", + "value": "492" + }, + { + "label": "comics", + "value": "1402" + }, + { + "label": "Conquer", + "value": "5059" + }, + { + "label": "Cyberpunk", + "value": "3294" + }, + { + "label": "ChildhoodP", + "value": "588" + }, + { + "label": "cosplaystr", + "value": "3551" + }, + { + "label": "CEO", + "value": "1008" + }, + { + "label": "Conditiona", + "value": "445" + }, + { + "label": "Confinemen", + "value": "172" + }, + { + "label": "ciweimao", + "value": "6168" + }, + { + "label": "crowmouth", + "value": "3552" + }, + { + "label": "ChoiceSele", + "value": "1308" + }, + { + "label": "Cousins", + "value": "683" + }, + { + "label": "CuriousPro", + "value": "261" + }, + { + "label": "catchfast", + "value": "3359" + }, + { + "label": "ComingofAg", + "value": "722" + }, + { + "label": "CampusLove", + "value": "589" + }, + { + "label": "ChinaEnter", + "value": "3549" + }, + { + "label": "Clubs", + "value": "611" + }, + { + "label": "cheating", + "value": "6149" + }, + { + "label": "captain", + "value": "3290" + }, + { + "label": "Co-Workers", + "value": "662" + }, + { + "label": "Crossing", + "value": "2901" + }, + { + "label": "Crosstalka", + "value": "3122" + }, + { + "label": "Creation", + "value": "1251" + }, + { + "label": "Cnnilingus", + "value": "1031" + }, + { + "label": "Creatures", + "value": "526" + }, + { + "label": "Coma", + "value": "105" + }, + { + "label": "Cityurban", + "value": "6159" + }, + { + "label": "ChaoPrimeM", + "value": "5750" + }, + { + "label": "College", + "value": "1454" + }, + { + "label": "contempora", + "value": "1400" + }, + { + "label": "CoolText", + "value": "1190" + }, + { + "label": "CollegeorU", + "value": "976" + }, + { + "label": "Chuunibyou", + "value": "713" + }, + { + "label": "CautiousMc", + "value": "1095" + }, + { + "label": "Childbirth", + "value": "1074" + }, + { + "label": "Conflictin", + "value": "541" + }, + { + "label": "CinderHand", + "value": "5755" + }, + { + "label": "chestnutch", + "value": "5105" + }, + { + "label": "cardleague", + "value": "4692" + }, + { + "label": "CrazySanMe", + "value": "4578" + }, + { + "label": "Cleaningfi", + "value": "4476" + }, + { + "label": "codewordgi", + "value": "4201" + }, + { + "label": "candlecher", + "value": "4160" + }, + { + "label": "catontheba", + "value": "4122" + }, + { + "label": "cuckoobunn", + "value": "4005" + }, + { + "label": "can&039", + "value": "3927" + }, + { + "label": "crazysquid", + "value": "3652" + }, + { + "label": "cunning", + "value": "2897" + }, + { + "label": "ColdNightL", + "value": "2819" + }, + { + "label": "chasingthe", + "value": "2777" + }, + { + "label": "cutepomelo", + "value": "2760" + }, + { + "label": "catloveson", + "value": "2536" + }, + { + "label": "Cloudseest", + "value": "2047" + }, + { + "label": "chaoticwor", + "value": "1882" + }, + { + "label": "cunningmc", + "value": "1759" + }, + { + "label": "Civilizati", + "value": "1613" + }, + { + "label": "Creator", + "value": "1549" + }, + { + "label": "CuteChild", + "value": "1452" + }, + { + "label": "Cultivator", + "value": "1361" + }, + { + "label": "CampusLife", + "value": "1274" + }, + { + "label": "CrazyProta", + "value": "1249" + }, + { + "label": "CosmicHorr", + "value": "1248" + }, + { + "label": "cunningfem", + "value": "1232" + }, + { + "label": "ColdLoveIn", + "value": "1106" + }, + { + "label": "CommonerLi", + "value": "950" + }, + { + "label": "ComedicUnd", + "value": "926" + }, + { + "label": "Cryostasis", + "value": "741" + }, + { + "label": "Cards", + "value": "564" + }, + { + "label": "city", + "value": "6130" + }, + { + "label": "Cutemeow-c", + "value": "6121" + }, + { + "label": "ConantheTh", + "value": "6113" + }, + { + "label": "crashreinc", + "value": "6110" + }, + { + "label": "carafe", + "value": "6101" + }, + { + "label": "ClydeVirgi", + "value": "6078" + }, + { + "label": "cyclingwin", + "value": "6074" + }, + { + "label": "Crazylittl", + "value": "6064" + }, + { + "label": "ChenTing", + "value": "6053" + }, + { + "label": "cuteflower", + "value": "6047" + }, + { + "label": "Captainoft", + "value": "6031" + }, + { + "label": "Cutelittle", + "value": "6019" + }, + { + "label": "ChevaRoy", + "value": "6017" + }, + { + "label": "ComicMonst", + "value": "6002" + }, + { + "label": "财女", + "value": "5989" + }, + { + "label": "Cocoawithf", + "value": "5983" + }, + { + "label": "Conan", + "value": "5972" + }, + { + "label": "CandiedLem", + "value": "5969" + }, + { + "label": "Chang&0", + "value": "5949" + }, + { + "label": "Conanisnot", + "value": "5946" + }, + { + "label": "Coolchubby", + "value": "5918" + }, + { + "label": "丑颜", + "value": "5890" + }, + { + "label": "CuredChick", + "value": "5860" + }, + { + "label": "cabbagebut", + "value": "5829" + }, + { + "label": "can&039", + "value": "5827" + }, + { + "label": "carrotsdon", + "value": "5804" + }, + { + "label": "catwatchin", + "value": "5795" + }, + { + "label": "cutelittle", + "value": "5793" + }, + { + "label": "chaotichea", + "value": "5788" + }, + { + "label": "Catisnotat", + "value": "5760" + }, + { + "label": "Catsandcat", + "value": "5753" + }, + { + "label": "catheadsan", + "value": "5741" + }, + { + "label": "ChenChen", + "value": "5734" + }, + { + "label": "☆Crazy→M", + "value": "5728" + }, + { + "label": "CarrotCake", + "value": "5724" + }, + { + "label": "Caothiefne", + "value": "5666" + }, + { + "label": "confusedfi", + "value": "5637" + }, + { + "label": "chugong", + "value": "5605" + }, + { + "label": "Conanadrea", + "value": "5604" + }, + { + "label": "ChuXiaNo.X", + "value": "5573" + }, + { + "label": "celeryeats", + "value": "5563" + }, + { + "label": "CastlePeak", + "value": "5559" + }, + { + "label": "Cheesemous", + "value": "5534" + }, + { + "label": "蝉眠", + "value": "5514" + }, + { + "label": "changedhea", + "value": "5511" + }, + { + "label": "Cheesesnow", + "value": "5478" + }, + { + "label": "carpenter", + "value": "5453" + }, + { + "label": "ChengWangH", + "value": "5443" + }, + { + "label": "Cyclospori", + "value": "5429" + }, + { + "label": "colorless", + "value": "5425" + }, + { + "label": "Crazy&0", + "value": "5406" + }, + { + "label": "commemorat", + "value": "5384" + }, + { + "label": "ConchBay", + "value": "5367" + }, + { + "label": "Cat", + "value": "5364" + }, + { + "label": "ChenZijin", + "value": "5363" + }, + { + "label": "CrossRebir", + "value": "5344" + }, + { + "label": "comprehens", + "value": "5341" + }, + { + "label": "Crossfarmi", + "value": "5323" + }, + { + "label": "CrossOverh", + "value": "5319" + }, + { + "label": "CrossGiant", + "value": "5307" + }, + { + "label": "CuckooChen", + "value": "5302" + }, + { + "label": "crowastron", + "value": "5283" + }, + { + "label": "CastlePeak", + "value": "5282" + }, + { + "label": "Completebo", + "value": "5278" + }, + { + "label": "coldnoodle", + "value": "5219" + }, + { + "label": "Chunghwape", + "value": "5200" + }, + { + "label": "chromatics", + "value": "5155" + }, + { + "label": "catwithbla", + "value": "5138" + }, + { + "label": "cloudpierc", + "value": "5106" + }, + { + "label": "coolmeowma", + "value": "5101" + }, + { + "label": "CatchaGhos", + "value": "5071" + }, + { + "label": "Canyoustop", + "value": "5032" + }, + { + "label": "CiShuhua", + "value": "5021" + }, + { + "label": "colorfasti", + "value": "5006" + }, + { + "label": "Candleligh", + "value": "4996" + }, + { + "label": "callmethep", + "value": "4993" + }, + { + "label": "CosmicChar", + "value": "4990" + }, + { + "label": "cutewhite", + "value": "4978" + }, + { + "label": "cloud-like", + "value": "4976" + }, + { + "label": "Crayfishan", + "value": "4958" + }, + { + "label": "can&039", + "value": "4944" + }, + { + "label": "codewordna", + "value": "4919" + }, + { + "label": "catisland", + "value": "4900" + }, + { + "label": "catcatfrui", + "value": "4881" + }, + { + "label": "Chirpingan", + "value": "4872" + }, + { + "label": "CrimsonFir", + "value": "4866" + }, + { + "label": "CardinalSa", + "value": "4861" + }, + { + "label": "catworship", + "value": "4849" + }, + { + "label": "CitySky1", + "value": "4819" + }, + { + "label": "ChicSu", + "value": "4754" + }, + { + "label": "CrimsonMoo", + "value": "4746" + }, + { + "label": "cucumber", + "value": "4741" + }, + { + "label": "CatCarMK2", + "value": "4730" + }, + { + "label": "Chengnanex", + "value": "4718" + }, + { + "label": "C14H18N2O5", + "value": "4693" + }, + { + "label": "CangsongTa", + "value": "4681" + }, + { + "label": "Chestnutca", + "value": "4675" + }, + { + "label": "cloudymoon", + "value": "4674" + }, + { + "label": "Carola", + "value": "4641" + }, + { + "label": "ChenDashun", + "value": "4608" + }, + { + "label": "captaindra", + "value": "4587" + }, + { + "label": "caterpilla", + "value": "4579" + }, + { + "label": "ChenYibing", + "value": "4566" + }, + { + "label": "cateatingp", + "value": "4535" + }, + { + "label": "CitrusRoas", + "value": "4533" + }, + { + "label": "ccc", + "value": "4511" + }, + { + "label": "Canbeheart", + "value": "4424" + }, + { + "label": "coolinocto", + "value": "4383" + }, + { + "label": "Chika", + "value": "4332" + }, + { + "label": "Chanyi", + "value": "4305" + }, + { + "label": "Clivia", + "value": "4292" + }, + { + "label": "coffeevi", + "value": "4230" + }, + { + "label": "cobrasnake", + "value": "4228" + }, + { + "label": "coldsalted", + "value": "4200" + }, + { + "label": "cutthrough", + "value": "4196" + }, + { + "label": "child", + "value": "4193" + }, + { + "label": "Conan&0", + "value": "4187" + }, + { + "label": "colorfulcl", + "value": "4182" + }, + { + "label": "CodewordG", + "value": "4179" + }, + { + "label": "Commanderd", + "value": "4163" + }, + { + "label": "chiccatles", + "value": "4154" + }, + { + "label": "coldinearl", + "value": "4128" + }, + { + "label": "Casual", + "value": "4125" + }, + { + "label": "ChangGuxue", + "value": "4118" + }, + { + "label": "cornisripe", + "value": "4109" + }, + { + "label": "ChaoGezi", + "value": "4100" + }, + { + "label": "crackedegg", + "value": "4085" + }, + { + "label": "crimsonsku", + "value": "4076" + }, + { + "label": "createpreh", + "value": "4052" + }, + { + "label": "catchthecl", + "value": "4030" + }, + { + "label": "Conservati", + "value": "4001" + }, + { + "label": "Competingf", + "value": "3928" + }, + { + "label": "Chiyudoesn", + "value": "3874" + }, + { + "label": "crazydream", + "value": "3829" + }, + { + "label": "ChefEmiya", + "value": "3818" + }, + { + "label": "ConanXiang", + "value": "3802" + }, + { + "label": "CrookedPay", + "value": "3798" + }, + { + "label": "Cultivatio", + "value": "3790" + }, + { + "label": "curechildr", + "value": "3789" + }, + { + "label": "chargingca", + "value": "3774" + }, + { + "label": "callmeknig", + "value": "3755" + }, + { + "label": "crimesolvi", + "value": "3733" + }, + { + "label": "chatter", + "value": "3668" + }, + { + "label": "ChiefScien", + "value": "3663" + }, + { + "label": "Ceobeisthe", + "value": "3653" + }, + { + "label": "catorange", + "value": "3648" + }, + { + "label": "Cutthelone", + "value": "3647" + }, + { + "label": "Can&039", + "value": "3535" + }, + { + "label": "corgishort", + "value": "3502" + }, + { + "label": "Canepepper", + "value": "3464" + }, + { + "label": "Conspiracy", + "value": "3435" + }, + { + "label": "Cultivatio", + "value": "3417" + }, + { + "label": "Confuciani", + "value": "3384" + }, + { + "label": "Childhoodf", + "value": "3378" + }, + { + "label": "civilizati", + "value": "3221" + }, + { + "label": "Criminalin", + "value": "3185" + }, + { + "label": "Crosssyste", + "value": "3088" + }, + { + "label": "ClassicXia", + "value": "3086" + }, + { + "label": "Cross空间O", + "value": "3084" + }, + { + "label": "Cross1V1st", + "value": "3072" + }, + { + "label": "Comprehens", + "value": "3071" + }, + { + "label": "CrossRebir", + "value": "3070" + }, + { + "label": "CrossChatg", + "value": "3066" + }, + { + "label": "CrossInvin", + "value": "3064" + }, + { + "label": "Crosssyste", + "value": "3058" + }, + { + "label": "comprehens", + "value": "3047" + }, + { + "label": "Crosswar", + "value": "3044" + }, + { + "label": "Comprehens", + "value": "3041" + }, + { + "label": "Crossmanyf", + "value": "3038" + }, + { + "label": "Crosssweet", + "value": "3027" + }, + { + "label": "comprehens", + "value": "3021" + }, + { + "label": "Crossfarmi", + "value": "3007" + }, + { + "label": "CrossMengB", + "value": "3005" + }, + { + "label": "CrossRelax", + "value": "3004" + }, + { + "label": "comprehens", + "value": "3003" + }, + { + "label": "CrossRebir", + "value": "3001" + }, + { + "label": "Crosssyste", + "value": "3000" + }, + { + "label": "Cross1V1HE", + "value": "2996" + }, + { + "label": "Crossshort", + "value": "2991" + }, + { + "label": "Crossmarti", + "value": "2980" + }, + { + "label": "comprehens", + "value": "2974" + }, + { + "label": "Crossfarmi", + "value": "2973" + }, + { + "label": "CrossGoldf", + "value": "2971" + }, + { + "label": "Comprehens", + "value": "2969" + }, + { + "label": "Cross空间u", + "value": "2968" + }, + { + "label": "Crossopeni", + "value": "2965" + }, + { + "label": "Crossgeniu", + "value": "2963" + }, + { + "label": "contractma", + "value": "2959" + }, + { + "label": "CrossSlapC", + "value": "2956" + }, + { + "label": "Crossmarti", + "value": "2955" + }, + { + "label": "CrossInvin", + "value": "2953" + }, + { + "label": "Comprehens", + "value": "2952" + }, + { + "label": "CrossRebir", + "value": "2951" + }, + { + "label": "CrossCount", + "value": "2947" + }, + { + "label": "ClassicXia", + "value": "2946" + }, + { + "label": "Crossevery", + "value": "2944" + }, + { + "label": "CollegeStr", + "value": "2940" + }, + { + "label": "CrossInvin", + "value": "2939" + }, + { + "label": "CangxueFei", + "value": "2932" + }, + { + "label": "Cruel", + "value": "2920" + }, + { + "label": "ChainsawMa", + "value": "2889" + }, + { + "label": "CampusRoma", + "value": "2880" + }, + { + "label": "cutelovein", + "value": "2875" + }, + { + "label": "cornjuice", + "value": "2815" + }, + { + "label": "Cupola", + "value": "2802" + }, + { + "label": "cateatingp", + "value": "2787" + }, + { + "label": "Cantaloupe", + "value": "2782" + }, + { + "label": "crookeddoo", + "value": "2772" + }, + { + "label": "cloudmadeo", + "value": "2771" + }, + { + "label": "Codeuntilt", + "value": "2746" + }, + { + "label": "city​​ya", + "value": "2738" + }, + { + "label": "cockroache", + "value": "2726" + }, + { + "label": "CarambolaJ", + "value": "2700" + }, + { + "label": "codewordge", + "value": "2693" + }, + { + "label": "CorpseFrag", + "value": "2663" + }, + { + "label": "Caicolorsh", + "value": "2640" + }, + { + "label": "catpowerfi", + "value": "2624" + }, + { + "label": "Can&039", + "value": "2623" + }, + { + "label": "CelestialC", + "value": "2608" + }, + { + "label": "catisrisin", + "value": "2568" + }, + { + "label": "civetcatat", + "value": "2562" + }, + { + "label": "Chosen12", + "value": "2533" + }, + { + "label": "coffeeinst", + "value": "2525" + }, + { + "label": "coldcolddo", + "value": "2473" + }, + { + "label": "catgod", + "value": "2470" + }, + { + "label": "CokeII", + "value": "2461" + }, + { + "label": "callthebea", + "value": "2417" + }, + { + "label": "Can&039", + "value": "2409" + }, + { + "label": "Crazystory", + "value": "2403" + }, + { + "label": "championge", + "value": "2368" + }, + { + "label": "ChenChangf", + "value": "2342" + }, + { + "label": "Cicadasand", + "value": "2340" + }, + { + "label": "catthousan", + "value": "2333" + }, + { + "label": "Cancat", + "value": "2292" + }, + { + "label": "CityGod", + "value": "2288" + }, + { + "label": "Catswithfi", + "value": "2271" + }, + { + "label": "Comprehens", + "value": "2243" + }, + { + "label": "Canteendry", + "value": "2228" + }, + { + "label": "Crazyforam", + "value": "2224" + }, + { + "label": "Changeever", + "value": "2223" + }, + { + "label": "Cloudtop丨", + "value": "2202" + }, + { + "label": "ChefSurviv", + "value": "2174" + }, + { + "label": "cartoonwil", + "value": "2168" + }, + { + "label": "cutegrapef", + "value": "2149" + }, + { + "label": "Catchtheca", + "value": "2141" + }, + { + "label": "CloudTop丨", + "value": "2137" + }, + { + "label": "caviar", + "value": "2131" + }, + { + "label": "ChenTwelve", + "value": "2122" + }, + { + "label": "Comeon", + "value": "2112" + }, + { + "label": "coldrivers", + "value": "2106" + }, + { + "label": "chef&03", + "value": "2097" + }, + { + "label": "Cloudtopfi", + "value": "2092" + }, + { + "label": "coffeefatc", + "value": "2046" + }, + { + "label": "catthatwan", + "value": "2022" + }, + { + "label": "canfly", + "value": "2019" + }, + { + "label": "camera", + "value": "2004" + }, + { + "label": "CloudSummi", + "value": "2000" + }, + { + "label": "Canolaflow", + "value": "1985" + }, + { + "label": "coyote", + "value": "1984" + }, + { + "label": "cuteshadow", + "value": "1972" + }, + { + "label": "ChiDongdon", + "value": "1966" + }, + { + "label": "CherryBlos", + "value": "1965" + }, + { + "label": "CloudTop丨", + "value": "1948" + }, + { + "label": "cloudysky", + "value": "1926" + }, + { + "label": "ChocolateI", + "value": "1922" + }, + { + "label": "CucumberHa", + "value": "1916" + }, + { + "label": "catdaylist", + "value": "1895" + }, + { + "label": "ColdStar&a", + "value": "1871" + }, + { + "label": "Chirika", + "value": "1843" + }, + { + "label": "coverthesu", + "value": "1834" + }, + { + "label": "coffeewith", + "value": "1802" + }, + { + "label": "Civilizati", + "value": "1775" + }, + { + "label": "codegeass", + "value": "1772" + }, + { + "label": "chat", + "value": "1757" + }, + { + "label": "CutePet", + "value": "1731" + }, + { + "label": "Contagonis", + "value": "1709" + }, + { + "label": "ChenHegao", + "value": "1708" + }, + { + "label": "ChinaNamba", + "value": "1705" + }, + { + "label": "ChineseAnc", + "value": "1692" + }, + { + "label": "Chivalryof", + "value": "1669" + }, + { + "label": "cluthullu", + "value": "1663" + }, + { + "label": "ChildhoodE", + "value": "1622" + }, + { + "label": "Competitio", + "value": "1607" + }, + { + "label": "Club", + "value": "1583" + }, + { + "label": "CunningPro", + "value": "1568" + }, + { + "label": "Comic", + "value": "1567" + }, + { + "label": "CaringMale", + "value": "1553" + }, + { + "label": "CuteMaleLe", + "value": "1522" + }, + { + "label": "CuteProtag", + "value": "1517" + }, + { + "label": "Conspirati", + "value": "1515" + }, + { + "label": "ChenShiAi", + "value": "3736" + }, + { + "label": "CivilServa", + "value": "1491" + }, + { + "label": "Capitalism", + "value": "1490" + }, + { + "label": "celestial", + "value": "1439" + }, + { + "label": "comics", + "value": "1423" + }, + { + "label": "competitiv", + "value": "1422" + }, + { + "label": "cultivatio", + "value": "1417" + }, + { + "label": "conquer", + "value": "1414" + }, + { + "label": "ChuningMC", + "value": "1390" + }, + { + "label": "CluelessPr", + "value": "1365" + }, + { + "label": "Chaos", + "value": "1360" + }, + { + "label": "Church", + "value": "1359" + }, + { + "label": "ChinaRefor", + "value": "1357" + }, + { + "label": "Cluelessly", + "value": "1322" + }, + { + "label": "Complicate", + "value": "1291" + }, + { + "label": "Companies", + "value": "1286" + }, + { + "label": "chat-room", + "value": "1285" + }, + { + "label": "Colonializ", + "value": "1201" + }, + { + "label": "Contract", + "value": "1163" + }, + { + "label": "ChildhoodS", + "value": "1155" + }, + { + "label": "CleverMc", + "value": "1142" + }, + { + "label": "Complaint", + "value": "1134" + }, + { + "label": "Constructi", + "value": "1113" + }, + { + "label": "CareerOrie", + "value": "1061" + }, + { + "label": "CoolMc", + "value": "1050" + }, + { + "label": "Charlotte(", + "value": "1041" + }, + { + "label": "ColdMalePr", + "value": "1033" + }, + { + "label": "ComplexFam", + "value": "956" + }, + { + "label": "ContractLo", + "value": "952" + }, + { + "label": "CubRaising", + "value": "916" + }, + { + "label": "ColdMaleLe", + "value": "904" + }, + { + "label": "Colonizati", + "value": "858" + }, + { + "label": "CalmMalePr", + "value": "834" + }, + { + "label": "Criminolog", + "value": "832" + }, + { + "label": "Commandand", + "value": "810" + }, + { + "label": "Celestials", + "value": "799" + }, + { + "label": "Curse", + "value": "785" + }, + { + "label": "Collection", + "value": "749" + }, + { + "label": "Cosplay", + "value": "726" + }, + { + "label": "CleverProt", + "value": "373" + }, + { + "label": "ComplexFam", + "value": "253" + }, + { + "label": "Charismati", + "value": "39" + }, + { + "label": "DouroConti", + "value": "3182" + }, + { + "label": "Doujinshi", + "value": "5675" + }, + { + "label": "Demons", + "value": "219" + }, + { + "label": "DevotedLov", + "value": "24" + }, + { + "label": "DotingLove", + "value": "26" + }, + { + "label": "Dining", + "value": "3181" + }, + { + "label": "Dragons", + "value": "310" + }, + { + "label": "Develop", + "value": "3246" + }, + { + "label": "Doctors", + "value": "25" + }, + { + "label": "directdaug", + "value": "3162" + }, + { + "label": "Disguiseas", + "value": "3245" + }, + { + "label": "Dotingwife", + "value": "3267" + }, + { + "label": "Depictions", + "value": "156" + }, + { + "label": "DenseProta", + "value": "65" + }, + { + "label": "Dark", + "value": "319" + }, + { + "label": "Drama", + "value": "885" + }, + { + "label": "doctorstre", + "value": "3165" + }, + { + "label": "DouluoDalu", + "value": "326" + }, + { + "label": "DotingPare", + "value": "178" + }, + { + "label": "DeathofLov", + "value": "64" + }, + { + "label": "Doublebusi", + "value": "3313" + }, + { + "label": "Dungeons", + "value": "632" + }, + { + "label": "DemonLord", + "value": "218" + }, + { + "label": "Detectiver", + "value": "3223" + }, + { + "label": "Divorce", + "value": "44" + }, + { + "label": "DotingOlde", + "value": "177" + }, + { + "label": "Death", + "value": "194" + }, + { + "label": "Detectives", + "value": "180" + }, + { + "label": "Demon", + "value": "1230" + }, + { + "label": "DaoCompreh", + "value": "583" + }, + { + "label": "Demi-Human", + "value": "430" + }, + { + "label": "doublebirt", + "value": "3341" + }, + { + "label": "derivative", + "value": "3388" + }, + { + "label": "Daoism", + "value": "444" + }, + { + "label": "Discrimina", + "value": "504" + }, + { + "label": "Dwarfs", + "value": "311" + }, + { + "label": "Disabiliti", + "value": "263" + }, + { + "label": "DomesticAf", + "value": "414" + }, + { + "label": "DetectiveC", + "value": "1347" + }, + { + "label": "DragonBall", + "value": "1309" + }, + { + "label": "Destiny", + "value": "342" + }, + { + "label": "Dragon", + "value": "1005" + }, + { + "label": "Detective", + "value": "935" + }, + { + "label": "DND", + "value": "3199" + }, + { + "label": "DaoCompani", + "value": "417" + }, + { + "label": "D", + "value": "4194" + }, + { + "label": "dandy", + "value": "3159" + }, + { + "label": "Dreams", + "value": "494" + }, + { + "label": "Depression", + "value": "303" + }, + { + "label": "DiscipleTr", + "value": "1318" + }, + { + "label": "DC", + "value": "835" + }, + { + "label": "Drugs", + "value": "543" + }, + { + "label": "DollsPuppe", + "value": "212" + }, + { + "label": "Designer", + "value": "3385" + }, + { + "label": "Divination", + "value": "535" + }, + { + "label": "DragonSlay", + "value": "309" + }, + { + "label": "Doujipin", + "value": "6147" + }, + { + "label": "Doomsday", + "value": "1080" + }, + { + "label": "DarkFantas", + "value": "922" + }, + { + "label": "Dwarves", + "value": "783" + }, + { + "label": "DemonSlaye", + "value": "1124" + }, + { + "label": "DungeonMas", + "value": "742" + }, + { + "label": "DeadProtag", + "value": "723" + }, + { + "label": "Delinquent", + "value": "247" + }, + { + "label": "Daughterof", + "value": "3893" + }, + { + "label": "door", + "value": "3451" + }, + { + "label": "Doctor", + "value": "1236" + }, + { + "label": "Dystopia", + "value": "618" + }, + { + "label": "Debts", + "value": "515" + }, + { + "label": "Devils", + "value": "800" + }, + { + "label": "DishonestP", + "value": "516" + }, + { + "label": "Druids", + "value": "506" + }, + { + "label": "Disfigurem", + "value": "634" + }, + { + "label": "Devil", + "value": "5081" + }, + { + "label": "dotinglove", + "value": "1373" + }, + { + "label": "Dinosaurs", + "value": "1122" + }, + { + "label": "Danmei", + "value": "758" + }, + { + "label": "Dream", + "value": "606" + }, + { + "label": "Dancers", + "value": "380" + }, + { + "label": "DragonRide", + "value": "308" + }, + { + "label": "DeathofDae", + "value": "4172" + }, + { + "label": "Dynasty", + "value": "3436" + }, + { + "label": "DeepLTrans", + "value": "1371" + }, + { + "label": "Director", + "value": "1192" + }, + { + "label": "DivineProt", + "value": "984" + }, + { + "label": "Doupo", + "value": "837" + }, + { + "label": "DotingPare", + "value": "830" + }, + { + "label": "嫡女", + "value": "6177" + }, + { + "label": "DemonHunte", + "value": "5175" + }, + { + "label": "Dongfengbu", + "value": "4363" + }, + { + "label": "Doyouwanta", + "value": "4007" + }, + { + "label": "dwatermelo", + "value": "3899" + }, + { + "label": "desertsalt", + "value": "3724" + }, + { + "label": "Desperate", + "value": "3503" + }, + { + "label": "Dayfestiva", + "value": "3480" + }, + { + "label": "Doraemon", + "value": "2890" + }, + { + "label": "dreamblizz", + "value": "2812" + }, + { + "label": "Daybyday", + "value": "2545" + }, + { + "label": "Demondomai", + "value": "2406" + }, + { + "label": "Destroyerf", + "value": "2309" + }, + { + "label": "Dreamofthe", + "value": "2153" + }, + { + "label": "digitalold", + "value": "2021" + }, + { + "label": "Digimon", + "value": "1767" + }, + { + "label": "DoupoBTTH", + "value": "826" + }, + { + "label": "Delusions", + "value": "640" + }, + { + "label": "DarkDeatho", + "value": "574" + }, + { + "label": "Doublebusi", + "value": "6199" + }, + { + "label": "Deadwoodwa", + "value": "6109" + }, + { + "label": "Dodosaurus", + "value": "6082" + }, + { + "label": "don&039", + "value": "6056" + }, + { + "label": "Don’taske", + "value": "6049" + }, + { + "label": "DestinyDra", + "value": "6029" + }, + { + "label": "DaoguangEt", + "value": "6022" + }, + { + "label": "doublepupp", + "value": "5950" + }, + { + "label": "东晨", + "value": "5945" + }, + { + "label": "Dreamstrin", + "value": "5938" + }, + { + "label": "don’tgowi", + "value": "5916" + }, + { + "label": "Diabloisno", + "value": "5912" + }, + { + "label": "DarkFaust", + "value": "5902" + }, + { + "label": "Duaninksto", + "value": "5850" + }, + { + "label": "divineradi", + "value": "5848" + }, + { + "label": "DemonKingL", + "value": "5847" + }, + { + "label": "dd", + "value": "5822" + }, + { + "label": "Dr.lifeadj", + "value": "5790" + }, + { + "label": "DanMing", + "value": "5783" + }, + { + "label": "DoDumbbell", + "value": "5738" + }, + { + "label": "Dayefire", + "value": "5726" + }, + { + "label": "don&039", + "value": "5671" + }, + { + "label": "Dawn&03", + "value": "5665" + }, + { + "label": "dragonball", + "value": "5649" + }, + { + "label": "dcc98", + "value": "5619" + }, + { + "label": "DidiDidi", + "value": "5607" + }, + { + "label": "desertspri", + "value": "5596" + }, + { + "label": "Doyouunder", + "value": "5595" + }, + { + "label": "Digigod", + "value": "5582" + }, + { + "label": "Dailyupdat", + "value": "5579" + }, + { + "label": "deepsea", + "value": "5551" + }, + { + "label": "DaqinTombR", + "value": "5541" + }, + { + "label": "disillusio", + "value": "5536" + }, + { + "label": "decayingro", + "value": "5501" + }, + { + "label": "DuckMantou", + "value": "5494" + }, + { + "label": "don&039", + "value": "5490" + }, + { + "label": "DongfangYu", + "value": "5483" + }, + { + "label": "DianZhongD", + "value": "5437" + }, + { + "label": "dragon&", + "value": "5392" + }, + { + "label": "dragongodd", + "value": "5377" + }, + { + "label": "DigJueJi", + "value": "5370" + }, + { + "label": "deadfishda", + "value": "5359" + }, + { + "label": "DaluoGod&a", + "value": "5353" + }, + { + "label": "desperatec", + "value": "5350" + }, + { + "label": "doublebirt", + "value": "5320" + }, + { + "label": "Deviationo", + "value": "5280" + }, + { + "label": "DustRainMa", + "value": "5267" + }, + { + "label": "DriedRadis", + "value": "5265" + }, + { + "label": "deepdream", + "value": "5257" + }, + { + "label": "dragonball", + "value": "5256" + }, + { + "label": "DaDaping", + "value": "5235" + }, + { + "label": "doyouseemy", + "value": "5216" + }, + { + "label": "don&039", + "value": "5206" + }, + { + "label": "DyeingBais", + "value": "5191" + }, + { + "label": "dreamstarr", + "value": "5187" + }, + { + "label": "DragonBall", + "value": "5183" + }, + { + "label": "Drunktime", + "value": "5165" + }, + { + "label": "DragonLord", + "value": "5160" + }, + { + "label": "DangeYihe", + "value": "5143" + }, + { + "label": "drinkmorew", + "value": "5139" + }, + { + "label": "driftingra", + "value": "5134" + }, + { + "label": "DrunkMoonS", + "value": "5113" + }, + { + "label": "DualCultiv", + "value": "5095" + }, + { + "label": "disability", + "value": "5045" + }, + { + "label": "DXNTotoro", + "value": "5040" + }, + { + "label": "devilcrayf", + "value": "4956" + }, + { + "label": "DevilWarri", + "value": "4953" + }, + { + "label": "Dirge", + "value": "4943" + }, + { + "label": "don&039", + "value": "4901" + }, + { + "label": "don&039", + "value": "4878" + }, + { + "label": "difficulty", + "value": "4799" + }, + { + "label": "darkfriedg", + "value": "4729" + }, + { + "label": "Drinkingam", + "value": "4698" + }, + { + "label": "Dejarimi", + "value": "4683" + }, + { + "label": "Dahuaisjea", + "value": "4682" + }, + { + "label": "Drunkmeet", + "value": "4635" + }, + { + "label": "dreambysta", + "value": "4597" + }, + { + "label": "DrivingHok", + "value": "4591" + }, + { + "label": "Doujinghos", + "value": "4562" + }, + { + "label": "Domineerin", + "value": "4555" + }, + { + "label": "drawTangpe", + "value": "4552" + }, + { + "label": "drawing123", + "value": "4494" + }, + { + "label": "dustpatter", + "value": "4487" + }, + { + "label": "DragonKing", + "value": "4459" + }, + { + "label": "Don&039", + "value": "4457" + }, + { + "label": "dreamnine", + "value": "4453" + }, + { + "label": "Dianhuo", + "value": "4439" + }, + { + "label": "Drunkencat", + "value": "4416" + }, + { + "label": "DecisiveMc", + "value": "4335" + }, + { + "label": "distantsky", + "value": "4225" + }, + { + "label": "dirtyadmir", + "value": "4174" + }, + { + "label": "Datangstyl", + "value": "4143" + }, + { + "label": "Dailyupdat", + "value": "4133" + }, + { + "label": "Doodle", + "value": "4130" + }, + { + "label": "dreamofmak", + "value": "4120" + }, + { + "label": "dryday", + "value": "4093" + }, + { + "label": "DamingComi", + "value": "4068" + }, + { + "label": "Destituteh", + "value": "4053" + }, + { + "label": "DurianGras", + "value": "3994" + }, + { + "label": "DragonWarr", + "value": "3992" + }, + { + "label": "dreamtogot", + "value": "3981" + }, + { + "label": "DongmenSno", + "value": "3941" + }, + { + "label": "Donothinga", + "value": "3934" + }, + { + "label": "DragonButt", + "value": "3919" + }, + { + "label": "don&039", + "value": "3880" + }, + { + "label": "dreamglaze", + "value": "3864" + }, + { + "label": "discordbir", + "value": "3860" + }, + { + "label": "Didicat", + "value": "3742" + }, + { + "label": "DrivingAdv", + "value": "3651" + }, + { + "label": "Dogcarp", + "value": "3644" + }, + { + "label": "don&039", + "value": "3624" + }, + { + "label": "Datanggodo", + "value": "3620" + }, + { + "label": "Dream101", + "value": "3575" + }, + { + "label": "DragonPowe", + "value": "3561" + }, + { + "label": "DemonPower", + "value": "3560" + }, + { + "label": "deepseadiv", + "value": "3545" + }, + { + "label": "DayKunpeng", + "value": "3517" + }, + { + "label": "don&039", + "value": "3496" + }, + { + "label": "drinkupLan", + "value": "3479" + }, + { + "label": "Decisive", + "value": "3012" + }, + { + "label": "DouroConti", + "value": "2992" + }, + { + "label": "Douluo", + "value": "2917" + }, + { + "label": "Domineerin", + "value": "2912" + }, + { + "label": "dimensiona", + "value": "2879" + }, + { + "label": "DivineBook", + "value": "2863" + }, + { + "label": "Don&039", + "value": "2861" + }, + { + "label": "dreamcatch", + "value": "2857" + }, + { + "label": "doyouwantc", + "value": "2856" + }, + { + "label": "Datangpota", + "value": "2845" + }, + { + "label": "Drunklifed", + "value": "2835" + }, + { + "label": "dirtylittl", + "value": "2739" + }, + { + "label": "DemonKing", + "value": "2690" + }, + { + "label": "DragonandL", + "value": "2672" + }, + { + "label": "DoctorData", + "value": "2657" + }, + { + "label": "Doomsdaywa", + "value": "2635" + }, + { + "label": "digthreefe", + "value": "2598" + }, + { + "label": "Donotforge", + "value": "2573" + }, + { + "label": "Donotbecon", + "value": "2566" + }, + { + "label": "Diga", + "value": "2554" + }, + { + "label": "Dikabenka", + "value": "2549" + }, + { + "label": "DriftwoodD", + "value": "2514" + }, + { + "label": "dagougou", + "value": "2513" + }, + { + "label": "dogeggsold", + "value": "2468" + }, + { + "label": "Daddywants", + "value": "2432" + }, + { + "label": "Dashuaihen", + "value": "2423" + }, + { + "label": "dreamintot", + "value": "2410" + }, + { + "label": "dragon-eat", + "value": "2397" + }, + { + "label": "DatangDaqi", + "value": "2366" + }, + { + "label": "Desperatel", + "value": "2354" + }, + { + "label": "Dahunjun", + "value": "2345" + }, + { + "label": "deadfatfas", + "value": "2318" + }, + { + "label": "doyoueator", + "value": "2295" + }, + { + "label": "Devilveget", + "value": "2293" + }, + { + "label": "DragonBall", + "value": "2289" + }, + { + "label": "Dollsister", + "value": "2250" + }, + { + "label": "DaqingXiao", + "value": "2238" + }, + { + "label": "DemonInvas", + "value": "2218" + }, + { + "label": "DaoyanShen", + "value": "2188" + }, + { + "label": "DamingYong", + "value": "2162" + }, + { + "label": "daughterco", + "value": "2084" + }, + { + "label": "don&039", + "value": "2055" + }, + { + "label": "DouTuKing", + "value": "2051" + }, + { + "label": "DragonPala", + "value": "2005" + }, + { + "label": "Datangsupe", + "value": "2003" + }, + { + "label": "DatangErwu", + "value": "1991" + }, + { + "label": "deepbluese", + "value": "1975" + }, + { + "label": "don&039", + "value": "1968" + }, + { + "label": "Decadeligh", + "value": "1958" + }, + { + "label": "dancetofig", + "value": "1955" + }, + { + "label": "DatangDaqi", + "value": "1938" + }, + { + "label": "dragracing", + "value": "1934" + }, + { + "label": "darknight", + "value": "1879" + }, + { + "label": "darkpirate", + "value": "1816" + }, + { + "label": "divinesign", + "value": "1804" + }, + { + "label": "dreamleave", + "value": "1797" + }, + { + "label": "DevotedCou", + "value": "1776" + }, + { + "label": "Dog", + "value": "1749" + }, + { + "label": "Devotedlov", + "value": "1735" + }, + { + "label": "DoingBusin", + "value": "1716" + }, + { + "label": "DisabledPr", + "value": "1693" + }, + { + "label": "DotingSibl", + "value": "1664" + }, + { + "label": "DanMachi", + "value": "1636" + }, + { + "label": "Disobedien", + "value": "1630" + }, + { + "label": "Dwarf", + "value": "1559" + }, + { + "label": "Doomdays", + "value": "1532" + }, + { + "label": "DestinedLo", + "value": "1531" + }, + { + "label": "Dominator", + "value": "1503" + }, + { + "label": "Diplomats", + "value": "1492" + }, + { + "label": "dotinghusb", + "value": "1396" + }, + { + "label": "DiscipleLo", + "value": "1377" + }, + { + "label": "dotingfami", + "value": "1372" + }, + { + "label": "differentw", + "value": "1367" + }, + { + "label": "DarkPower", + "value": "1363" + }, + { + "label": "dungeon", + "value": "1354" + }, + { + "label": "Doujin", + "value": "1293" + }, + { + "label": "DoubleRebi", + "value": "1227" + }, + { + "label": "Depictions", + "value": "1221" + }, + { + "label": "DoubleLife", + "value": "1185" + }, + { + "label": "Daughter", + "value": "1145" + }, + { + "label": "Determined", + "value": "1143" + }, + { + "label": "DemonsFami", + "value": "1108" + }, + { + "label": "Diplomacy", + "value": "1103" + }, + { + "label": "DumbProtag", + "value": "1071" + }, + { + "label": "DoulouDalu", + "value": "1038" + }, + { + "label": "doppelgang", + "value": "1010" + }, + { + "label": "DiggingtoS", + "value": "866" + }, + { + "label": "DouluoDolu", + "value": "836" + }, + { + "label": "Divination", + "value": "678" + }, + { + "label": "Distrustfu", + "value": "542" + }, + { + "label": "DemonicCul", + "value": "395" + }, + { + "label": "Determined", + "value": "165" + }, + { + "label": "DifferentS", + "value": "136" + }, + { + "label": "Entertainm", + "value": "228" + }, + { + "label": "everydayla", + "value": "3097" + }, + { + "label": "Evolution", + "value": "339" + }, + { + "label": "emperor", + "value": "3411" + }, + { + "label": "EarlyRoman", + "value": "2" + }, + { + "label": "Eschatolog", + "value": "3183" + }, + { + "label": "Elves", + "value": "354" + }, + { + "label": "Engage", + "value": "3283" + }, + { + "label": "EvilProtag", + "value": "382" + }, + { + "label": "evildoer", + "value": "3310" + }, + { + "label": "EnemiesBec", + "value": "195" + }, + { + "label": "Eat", + "value": "3311" + }, + { + "label": "Episodic", + "value": "157" + }, + { + "label": "ElementalM", + "value": "398" + }, + { + "label": "ESports", + "value": "5676" + }, + { + "label": "EvilGods", + "value": "333" + }, + { + "label": "Empress", + "value": "3257" + }, + { + "label": "evolutiona", + "value": "5703" + }, + { + "label": "EuropeanAm", + "value": "457" + }, + { + "label": "Elvish", + "value": "3247" + }, + { + "label": "EyePowers", + "value": "351" + }, + { + "label": "e-Sports", + "value": "275" + }, + { + "label": "emperorstr", + "value": "3143" + }, + { + "label": "enemy", + "value": "3296" + }, + { + "label": "Entertainm", + "value": "874" + }, + { + "label": "elvesandgh", + "value": "5689" + }, + { + "label": "Effort", + "value": "3356" + }, + { + "label": "EideticMem", + "value": "397" + }, + { + "label": "escapemarr", + "value": "3613" + }, + { + "label": "Empires", + "value": "437" + }, + { + "label": "EasternSet", + "value": "998" + }, + { + "label": "Economics", + "value": "505" + }, + { + "label": "EvilOrgani", + "value": "66" + }, + { + "label": "Eatchicken", + "value": "3704" + }, + { + "label": "Exorcism", + "value": "468" + }, + { + "label": "Engagement", + "value": "655" + }, + { + "label": "EnemiesBec", + "value": "213" + }, + { + "label": "EarthInvas", + "value": "712" + }, + { + "label": "EasyGoingL", + "value": "434" + }, + { + "label": "Europe", + "value": "3334" + }, + { + "label": "Exposurefl", + "value": "3137" + }, + { + "label": "Eunuch", + "value": "343" + }, + { + "label": "EvilReligi", + "value": "529" + }, + { + "label": "epicfantas", + "value": "5712" + }, + { + "label": "Elite", + "value": "1268" + }, + { + "label": "Engineer", + "value": "697" + }, + { + "label": "Ecchi", + "value": "5085" + }, + { + "label": "Exhaustion", + "value": "3847" + }, + { + "label": "easternfan", + "value": "693" + }, + { + "label": "Experience", + "value": "1334" + }, + { + "label": "Enlightenm", + "value": "577" + }, + { + "label": "Exorcist", + "value": "1193" + }, + { + "label": "Egoist", + "value": "5451" + }, + { + "label": "Elf", + "value": "1296" + }, + { + "label": "EvilGod", + "value": "1058" + }, + { + "label": "Exhibition", + "value": "1036" + }, + { + "label": "EagleRoadC", + "value": "5771" + }, + { + "label": "Eightswast", + "value": "4185" + }, + { + "label": "elegantlit", + "value": "4184" + }, + { + "label": "eveningdog", + "value": "3979" + }, + { + "label": "excellentp", + "value": "3851" + }, + { + "label": "Everyyear", + "value": "3761" + }, + { + "label": "EmperorCha", + "value": "2867" + }, + { + "label": "Evergrande", + "value": "2708" + }, + { + "label": "everlastin", + "value": "2678" + }, + { + "label": "engageinba", + "value": "2674" + }, + { + "label": "EnemytoLov", + "value": "1623" + }, + { + "label": "EuropeanAm", + "value": "946" + }, + { + "label": "ElderlyPro", + "value": "776" + }, + { + "label": "EconomicsE", + "value": "738" + }, + { + "label": "Entertainm", + "value": "6122" + }, + { + "label": "EmbraceofL", + "value": "6095" + }, + { + "label": "Endlesswin", + "value": "6061" + }, + { + "label": "Erhuowon’", + "value": "5919" + }, + { + "label": "emptyempty", + "value": "5900" + }, + { + "label": "Esper", + "value": "5889" + }, + { + "label": "evengod", + "value": "5768" + }, + { + "label": "eatmoremea", + "value": "5614" + }, + { + "label": "Eschatolog", + "value": "5340" + }, + { + "label": "Entertainm", + "value": "5327" + }, + { + "label": "Eschatolog", + "value": "5315" + }, + { + "label": "Eatsmallto", + "value": "5292" + }, + { + "label": "Entertainm", + "value": "5284" + }, + { + "label": "emptydream", + "value": "5275" + }, + { + "label": "Ergouzi&am", + "value": "5185" + }, + { + "label": "EndoftheWo", + "value": "5177" + }, + { + "label": "EternalLif", + "value": "5159" + }, + { + "label": "evilking", + "value": "5114" + }, + { + "label": "EvilSpirit", + "value": "5070" + }, + { + "label": "empressfem", + "value": "5047" + }, + { + "label": "eatfeet", + "value": "4964" + }, + { + "label": "Explodethe", + "value": "4945" + }, + { + "label": "EndlessSha", + "value": "4883" + }, + { + "label": "ExtremeAng", + "value": "4809" + }, + { + "label": "Ecstasy", + "value": "4771" + }, + { + "label": "EighteenRa", + "value": "4751" + }, + { + "label": "Europeansd", + "value": "4738" + }, + { + "label": "entertainm", + "value": "4637" + }, + { + "label": "eighthundr", + "value": "4609" + }, + { + "label": "Eveningwin", + "value": "4594" + }, + { + "label": "EvernightB", + "value": "4589" + }, + { + "label": "eighteight", + "value": "4541" + }, + { + "label": "emptymind", + "value": "4440" + }, + { + "label": "ExcerptKin", + "value": "4425" + }, + { + "label": "eatingmeat", + "value": "4421" + }, + { + "label": "Eroticmeow", + "value": "4404" + }, + { + "label": "elemental", + "value": "4350" + }, + { + "label": "eternalsta", + "value": "4320" + }, + { + "label": "edgecoatin", + "value": "4277" + }, + { + "label": "Everyonewh", + "value": "4199" + }, + { + "label": "Europeanem", + "value": "4188" + }, + { + "label": "emptyhate", + "value": "4132" + }, + { + "label": "emptystard", + "value": "4101" + }, + { + "label": "ErieShaggy", + "value": "3948" + }, + { + "label": "Emotionles", + "value": "3910" + }, + { + "label": "Eight-star", + "value": "3858" + }, + { + "label": "Eatasteril", + "value": "3850" + }, + { + "label": "eternallor", + "value": "3767" + }, + { + "label": "eatfishdon", + "value": "3658" + }, + { + "label": "eartwarmin", + "value": "3564" + }, + { + "label": "evensuture", + "value": "3523" + }, + { + "label": "Essencedre", + "value": "3477" + }, + { + "label": "Entertainm", + "value": "3083" + }, + { + "label": "everydayla", + "value": "3079" + }, + { + "label": "Eschatolog", + "value": "3074" + }, + { + "label": "Entertainm", + "value": "3060" + }, + { + "label": "Entertainm", + "value": "3037" + }, + { + "label": "Entertainm", + "value": "3030" + }, + { + "label": "Eschatolog", + "value": "3002" + }, + { + "label": "Entertainm", + "value": "2961" + }, + { + "label": "Eighteence", + "value": "2925" + }, + { + "label": "Eggplantan", + "value": "2792" + }, + { + "label": "entertainm", + "value": "2783" + }, + { + "label": "Erwazi", + "value": "2776" + }, + { + "label": "electricmo", + "value": "2628" + }, + { + "label": "entertainm", + "value": "2593" + }, + { + "label": "everydayfi", + "value": "2502" + }, + { + "label": "EndoftheWo", + "value": "2500" + }, + { + "label": "Entertaini", + "value": "2460" + }, + { + "label": "eternityor", + "value": "2457" + }, + { + "label": "emptymonol", + "value": "2407" + }, + { + "label": "Evergrande", + "value": "2352" + }, + { + "label": "Extremelyi", + "value": "2240" + }, + { + "label": "Eggpie", + "value": "2180" + }, + { + "label": "EmperorYao", + "value": "1937" + }, + { + "label": "Elfcold", + "value": "1905" + }, + { + "label": "EunuchJinr", + "value": "1830" + }, + { + "label": "enemiestol", + "value": "1771" + }, + { + "label": "electricia", + "value": "1769" + }, + { + "label": "exes", + "value": "1752" + }, + { + "label": "EvilSprits", + "value": "1712" + }, + { + "label": "entertainm", + "value": "1699" + }, + { + "label": "EatingBroa", + "value": "1631" + }, + { + "label": "Extraordin", + "value": "1603" + }, + { + "label": "Eccentricp", + "value": "1596" + }, + { + "label": "Easygoingp", + "value": "1557" + }, + { + "label": "Ex-girlfri", + "value": "1534" + }, + { + "label": "Emperialpo", + "value": "1513" + }, + { + "label": "Evil-prota", + "value": "1479" + }, + { + "label": "evolution", + "value": "1403" + }, + { + "label": "EvilOrgani", + "value": "1386" + }, + { + "label": "eyepower", + "value": "1383" + }, + { + "label": "EvilCharac", + "value": "1250" + }, + { + "label": "Empire", + "value": "1202" + }, + { + "label": "Entertaime", + "value": "1164" + }, + { + "label": "EnemytoLov", + "value": "1156" + }, + { + "label": "Elementali", + "value": "1006" + }, + { + "label": "Entertainm", + "value": "879" + }, + { + "label": "EmpireBuil", + "value": "859" + }, + { + "label": "Evil", + "value": "808" + }, + { + "label": "eincarnate", + "value": "789" + }, + { + "label": "Editors", + "value": "575" + }, + { + "label": "Emotionall", + "value": "381" + }, + { + "label": "Faloo", + "value": "1304" + }, + { + "label": "FemaleProt", + "value": "18" + }, + { + "label": "Farming", + "value": "229" + }, + { + "label": "fastpaced", + "value": "3144" + }, + { + "label": "Funny", + "value": "3138" + }, + { + "label": "Fan-fictio", + "value": "183" + }, + { + "label": "Fantasy", + "value": "391" + }, + { + "label": "fastwear", + "value": "3146" + }, + { + "label": "Fanfiction", + "value": "184" + }, + { + "label": "Fanderivat", + "value": "6145" + }, + { + "label": "flood", + "value": "3220" + }, + { + "label": "femalematc", + "value": "3121" + }, + { + "label": "FantasyWor", + "value": "277" + }, + { + "label": "FastCultiv", + "value": "67" + }, + { + "label": "fanqienove", + "value": "5009" + }, + { + "label": "Futuristic", + "value": "126" + }, + { + "label": "Fightagain", + "value": "3281" + }, + { + "label": "futureworl", + "value": "2909" + }, + { + "label": "Family", + "value": "148" + }, + { + "label": "FirstLove", + "value": "207" + }, + { + "label": "farmgate", + "value": "3160" + }, + { + "label": "FamilialLo", + "value": "291" + }, + { + "label": "Football", + "value": "352" + }, + { + "label": "flowercare", + "value": "3177" + }, + { + "label": "Friendship", + "value": "421" + }, + { + "label": "FamousProt", + "value": "383" + }, + { + "label": "FastLearne", + "value": "68" + }, + { + "label": "FamilyConf", + "value": "248" + }, + { + "label": "fiercewife", + "value": "3273" + }, + { + "label": "Farmer", + "value": "3196" + }, + { + "label": "FantasyCre", + "value": "362" + }, + { + "label": "Firearms", + "value": "166" + }, + { + "label": "FatedLover", + "value": "272" + }, + { + "label": "fullflow", + "value": "3404" + }, + { + "label": "FantasyMag", + "value": "532" + }, + { + "label": "Fightingpi", + "value": "5687" + }, + { + "label": "FamilyBusi", + "value": "459" + }, + { + "label": "fantasyhis", + "value": "3439" + }, + { + "label": "famous", + "value": "3427" + }, + { + "label": "Filmempero", + "value": "3368" + }, + { + "label": "First-time", + "value": "882" + }, + { + "label": "ForcedMarr", + "value": "111" + }, + { + "label": "Flourishin", + "value": "3369" + }, + { + "label": "Fukujin", + "value": "3339" + }, + { + "label": "FairyTail", + "value": "1023" + }, + { + "label": "FattoFit", + "value": "359" + }, + { + "label": "folksuspen", + "value": "3364" + }, + { + "label": "FaceSlappi", + "value": "914" + }, + { + "label": "finepoint", + "value": "3638" + }, + { + "label": "flirtatiou", + "value": "3616" + }, + { + "label": "Fightingfo", + "value": "6164" + }, + { + "label": "fox", + "value": "3433" + }, + { + "label": "Fanfic", + "value": "1194" + }, + { + "label": "firstaid", + "value": "3298" + }, + { + "label": "Fellatio", + "value": "629" + }, + { + "label": "FoxSpirits", + "value": "256" + }, + { + "label": "FearlessPr", + "value": "639" + }, + { + "label": "FemaleMast", + "value": "557" + }, + { + "label": "foodie", + "value": "3414" + }, + { + "label": "FutureCivi", + "value": "355" + }, + { + "label": "Furutake", + "value": "3607" + }, + { + "label": "FoodWars!", + "value": "1317" + }, + { + "label": "FamousPare", + "value": "627" + }, + { + "label": "Fairies", + "value": "584" + }, + { + "label": "FengShui", + "value": "581" + }, + { + "label": "Fishing", + "value": "1131" + }, + { + "label": "Fllatio", + "value": "191" + }, + { + "label": "femalehono", + "value": "3248" + }, + { + "label": "Finance", + "value": "2895" + }, + { + "label": "FemaleLead", + "value": "986" + }, + { + "label": "FatProtago", + "value": "358" + }, + { + "label": "Flowerprot", + "value": "6171" + }, + { + "label": "Flashbacks", + "value": "135" + }, + { + "label": "Filipino", + "value": "1682" + }, + { + "label": "FriendsBec", + "value": "737" + }, + { + "label": "fantasticf", + "value": "5708" + }, + { + "label": "Forensic", + "value": "3716" + }, + { + "label": "Faceslap", + "value": "1620" + }, + { + "label": "FallenNobi", + "value": "524" + }, + { + "label": "FilipinoNo", + "value": "1681" + }, + { + "label": "FemaletoMa", + "value": "734" + }, + { + "label": "FallenAnge", + "value": "596" + }, + { + "label": "Food", + "value": "814" + }, + { + "label": "FormerHero", + "value": "478" + }, + { + "label": "Funnycomme", + "value": "6198" + }, + { + "label": "非洲", + "value": "3714" + }, + { + "label": "FleetBattl", + "value": "745" + }, + { + "label": "Fujoshi", + "value": "69" + }, + { + "label": "floatingse", + "value": "4800" + }, + { + "label": "flowerdust", + "value": "4400" + }, + { + "label": "federalhea", + "value": "4236" + }, + { + "label": "fatcatunde", + "value": "4099" + }, + { + "label": "Future", + "value": "1698" + }, + { + "label": "FamilyBuil", + "value": "854" + }, + { + "label": "Familiars", + "value": "780" + }, + { + "label": "Folklore", + "value": "623" + }, + { + "label": "fusion", + "value": "5365" + }, + { + "label": "风水", + "value": "4863" + }, + { + "label": "faraway", + "value": "4607" + }, + { + "label": "FlyingDrag", + "value": "4458" + }, + { + "label": "FusoFantas", + "value": "4319" + }, + { + "label": "forestinth", + "value": "4291" + }, + { + "label": "Frozencorn", + "value": "4215" + }, + { + "label": "flyingnood", + "value": "3914" + }, + { + "label": "fingertips", + "value": "3877" + }, + { + "label": "fishandraf", + "value": "2481" + }, + { + "label": "Favoritebl", + "value": "2110" + }, + { + "label": "Fallenleav", + "value": "2108" + }, + { + "label": "FengziXiao", + "value": "2077" + }, + { + "label": "fatmanoffa", + "value": "2060" + }, + { + "label": "famouscoup", + "value": "1662" + }, + { + "label": "Farm", + "value": "1654" + }, + { + "label": "FastGrowth", + "value": "1264" + }, + { + "label": "Femaleprot", + "value": "1226" + }, + { + "label": "FarmingTex", + "value": "1218" + }, + { + "label": "Fanaticism", + "value": "710" + }, + { + "label": "ForgetfulP", + "value": "384" + }, + { + "label": "Futanari", + "value": "193" + }, + { + "label": "反穿", + "value": "6196" + }, + { + "label": "fqloo", + "value": "6183" + }, + { + "label": "Futureover", + "value": "6139" + }, + { + "label": "fantasyspa", + "value": "6136" + }, + { + "label": "Fantian36L", + "value": "6118" + }, + { + "label": "Favoriteco", + "value": "6100" + }, + { + "label": "Fortyacres", + "value": "6065" + }, + { + "label": "FantasyJi", + "value": "5997" + }, + { + "label": "Flowersblo", + "value": "5952" + }, + { + "label": "fancywords", + "value": "5935" + }, + { + "label": "frenchcoun", + "value": "5933" + }, + { + "label": "FifthQingl", + "value": "5931" + }, + { + "label": "Fomalhaut7", + "value": "5877" + }, + { + "label": "FrostZhong", + "value": "5867" + }, + { + "label": "Freshparad", + "value": "5864" + }, + { + "label": "Fromnowon", + "value": "5837" + }, + { + "label": "Fenwhoeats", + "value": "5810" + }, + { + "label": "flyburning", + "value": "5784" + }, + { + "label": "FatDragonJ", + "value": "5767" + }, + { + "label": "femaleprim", + "value": "5745" + }, + { + "label": "five-color", + "value": "5722" + }, + { + "label": "FiveDynast", + "value": "5702" + }, + { + "label": "Foldoffthi", + "value": "5668" + }, + { + "label": "Fairy丨Xiy", + "value": "5656" + }, + { + "label": "Fathouselo", + "value": "5647" + }, + { + "label": "fairytailb", + "value": "5583" + }, + { + "label": "Fantastico", + "value": "5570" + }, + { + "label": "foxcontrol", + "value": "5554" + }, + { + "label": "foxdemonpr", + "value": "5533" + }, + { + "label": "Forcedtomo", + "value": "5503" + }, + { + "label": "Frostandsn", + "value": "5489" + }, + { + "label": "Farmingthr", + "value": "5485" + }, + { + "label": "forgottoun", + "value": "5476" + }, + { + "label": "Fiction", + "value": "5450" + }, + { + "label": "Fatcatwhow", + "value": "5441" + }, + { + "label": "Fineartisl", + "value": "5424" + }, + { + "label": "flashhusky", + "value": "5405" + }, + { + "label": "fornoreaso", + "value": "5362" + }, + { + "label": "fastwearsw", + "value": "5346" + }, + { + "label": "fastwear1V", + "value": "5330" + }, + { + "label": "fastwearNo", + "value": "5310" + }, + { + "label": "fastwearIn", + "value": "5305" + }, + { + "label": "Flyingsnow", + "value": "5304" + }, + { + "label": "Fate90degr", + "value": "5287" + }, + { + "label": "FengTingyu", + "value": "5273" + }, + { + "label": "Flowingclo", + "value": "5258" + }, + { + "label": "Fierceadve", + "value": "5212" + }, + { + "label": "Fengzilike", + "value": "5205" + }, + { + "label": "FenglinYey", + "value": "5199" + }, + { + "label": "Fetish", + "value": "5094" + }, + { + "label": "Foursome", + "value": "5088" + }, + { + "label": "futuredyst", + "value": "5049" + }, + { + "label": "flirtyclou", + "value": "5036" + }, + { + "label": "fishinginf", + "value": "4972" + }, + { + "label": "Fanqin", + "value": "4950" + }, + { + "label": "Flowingclo", + "value": "4933" + }, + { + "label": "floatingdu", + "value": "4916" + }, + { + "label": "FireXinxin", + "value": "4902" + }, + { + "label": "FungMingHe", + "value": "4897" + }, + { + "label": "fool", + "value": "4871" + }, + { + "label": "Fuyuan", + "value": "4859" + }, + { + "label": "FriedJunhu", + "value": "4781" + }, + { + "label": "FogLan", + "value": "4732" + }, + { + "label": "Flyingmeat", + "value": "4710" + }, + { + "label": "Fragmentof", + "value": "4696" + }, + { + "label": "firstfragr", + "value": "4662" + }, + { + "label": "firefire", + "value": "4650" + }, + { + "label": "foldedkite", + "value": "4628" + }, + { + "label": "Fatmeowdoe", + "value": "4556" + }, + { + "label": "FengyueXun", + "value": "4543" + }, + { + "label": "Friedsquid", + "value": "4514" + }, + { + "label": "fate", + "value": "4488" + }, + { + "label": "FlashBonda", + "value": "4441" + }, + { + "label": "frenzyflow", + "value": "4417" + }, + { + "label": "fatorange", + "value": "4370" + }, + { + "label": "fasttravel", + "value": "4348" + }, + { + "label": "FemalePart", + "value": "4347" + }, + { + "label": "FantasyCre", + "value": "4343" + }, + { + "label": "FatHouseDu", + "value": "4328" + }, + { + "label": "Foggy", + "value": "4204" + }, + { + "label": "Formation", + "value": "4192" + }, + { + "label": "FlowerBlos", + "value": "4180" + }, + { + "label": "fallinlove", + "value": "4145" + }, + { + "label": "FairyTailR", + "value": "4137" + }, + { + "label": "flytothemo", + "value": "4124" + }, + { + "label": "Fishingmak", + "value": "4117" + }, + { + "label": "fragmentso", + "value": "4070" + }, + { + "label": "Fromthepup", + "value": "4063" + }, + { + "label": "fieldowner", + "value": "4046" + }, + { + "label": "Flyinggras", + "value": "4019" + }, + { + "label": "FemaleEmpe", + "value": "4012" + }, + { + "label": "Floatingcl", + "value": "3987" + }, + { + "label": "Flower&", + "value": "3967" + }, + { + "label": "Fairy丨San", + "value": "3946" + }, + { + "label": "favoritepo", + "value": "3915" + }, + { + "label": "Favoriteup", + "value": "3867" + }, + { + "label": "Fullyarmed", + "value": "3853" + }, + { + "label": "Fullcolor", + "value": "3841" + }, + { + "label": "favoritegr", + "value": "3814" + }, + { + "label": "Foxhimself", + "value": "3809" + }, + { + "label": "FocusSpeci", + "value": "3795" + }, + { + "label": "fried", + "value": "3739" + }, + { + "label": "FeitianOld", + "value": "3686" + }, + { + "label": "FuHejun", + "value": "3676" + }, + { + "label": "Fairyrefer", + "value": "3619" + }, + { + "label": "FengLingzo", + "value": "3585" + }, + { + "label": "Fairy", + "value": "3546" + }, + { + "label": "fishdragon", + "value": "3530" + }, + { + "label": "featherand", + "value": "3513" + }, + { + "label": "faintpenan", + "value": "3501" + }, + { + "label": "flamboyant", + "value": "3491" + }, + { + "label": "Fatestayni", + "value": "3419" + }, + { + "label": "foreignwei", + "value": "3261" + }, + { + "label": "fastwearPr", + "value": "3081" + }, + { + "label": "flowercare", + "value": "3077" + }, + { + "label": "farmingsys", + "value": "3067" + }, + { + "label": "fastwear1V", + "value": "3063" + }, + { + "label": "fastwearst", + "value": "3061" + }, + { + "label": "futureworl", + "value": "3056" + }, + { + "label": "floodRelax", + "value": "3045" + }, + { + "label": "fastwearCo", + "value": "3028" + }, + { + "label": "flowercare", + "value": "2997" + }, + { + "label": "futureworl", + "value": "2989" + }, + { + "label": "fastwearRe", + "value": "2960" + }, + { + "label": "FemaleProt", + "value": "2913" + }, + { + "label": "fisheatpan", + "value": "2869" + }, + { + "label": "Flyingwhit", + "value": "2862" + }, + { + "label": "formworksk", + "value": "2817" + }, + { + "label": "Fishheadis", + "value": "2814" + }, + { + "label": "fallintoth", + "value": "2813" + }, + { + "label": "Fifi&03", + "value": "2808" + }, + { + "label": "flowersoft", + "value": "2805" + }, + { + "label": "fishswimmi", + "value": "2779" + }, + { + "label": "FallenWing", + "value": "2767" + }, + { + "label": "Favoriteco", + "value": "2759" + }, + { + "label": "fishinflam", + "value": "2753" + }, + { + "label": "fanofstar", + "value": "2728" + }, + { + "label": "flyingshar", + "value": "2719" + }, + { + "label": "Faucet", + "value": "2707" + }, + { + "label": "FahaiInvin", + "value": "2660" + }, + { + "label": "flyinthelo", + "value": "2615" + }, + { + "label": "fireonfire", + "value": "2602" + }, + { + "label": "FatDiddy", + "value": "2594" + }, + { + "label": "fakegod", + "value": "2555" + }, + { + "label": "Floatingli", + "value": "2550" + }, + { + "label": "flyingcow", + "value": "2547" + }, + { + "label": "firstgreen", + "value": "2535" + }, + { + "label": "flyinglitt", + "value": "2522" + }, + { + "label": "Fantasybos", + "value": "2496" + }, + { + "label": "fairygirlf", + "value": "2480" + }, + { + "label": "FifthEmper", + "value": "2454" + }, + { + "label": "Fourkeys", + "value": "2451" + }, + { + "label": "FightingCo", + "value": "2450" + }, + { + "label": "Fengqing", + "value": "2425" + }, + { + "label": "FireWinged", + "value": "2384" + }, + { + "label": "FoxdemonXi", + "value": "2377" + }, + { + "label": "FangQingya", + "value": "2374" + }, + { + "label": "flyingsqui", + "value": "2364" + }, + { + "label": "furioussna", + "value": "2336" + }, + { + "label": "FlyingLuTi", + "value": "2301" + }, + { + "label": "FanJiu", + "value": "2272" + }, + { + "label": "FantaCola", + "value": "2265" + }, + { + "label": "Fahaiunder", + "value": "2251" + }, + { + "label": "Forgiveyou", + "value": "2211" + }, + { + "label": "fullmeal", + "value": "2173" + }, + { + "label": "flyingfish", + "value": "2146" + }, + { + "label": "forest", + "value": "2134" + }, + { + "label": "fierce", + "value": "2128" + }, + { + "label": "Followthew", + "value": "2091" + }, + { + "label": "fishfishda", + "value": "2070" + }, + { + "label": "Fireinthes", + "value": "2040" + }, + { + "label": "firstperso", + "value": "1989" + }, + { + "label": "FairySword", + "value": "1974" + }, + { + "label": "FeiLuEdiso", + "value": "1947" + }, + { + "label": "FallingRai", + "value": "1946" + }, + { + "label": "FerrariEnz", + "value": "1928" + }, + { + "label": "Famousdete", + "value": "1918" + }, + { + "label": "Friday", + "value": "1885" + }, + { + "label": "flamingfla", + "value": "1835" + }, + { + "label": "foxlisteni", + "value": "1827" + }, + { + "label": "Fairy丨Pin", + "value": "1824" + }, + { + "label": "Firethief", + "value": "1810" + }, + { + "label": "Fantasyfut", + "value": "1753" + }, + { + "label": "FamilyLove", + "value": "1676" + }, + { + "label": "familylife", + "value": "1591" + }, + { + "label": "FemaleFigh", + "value": "1586" + }, + { + "label": "FateSeries", + "value": "1585" + }, + { + "label": "FanFicton", + "value": "1545" + }, + { + "label": "Futuristic", + "value": "1500" + }, + { + "label": "France", + "value": "1496" + }, + { + "label": "FemaleSpie", + "value": "1493" + }, + { + "label": "FastWearin", + "value": "1466" + }, + { + "label": "FemalePres", + "value": "1458" + }, + { + "label": "fanfic", + "value": "1424" + }, + { + "label": "fireborn", + "value": "1407" + }, + { + "label": "fatedxd", + "value": "1404" + }, + { + "label": "FemaleMC", + "value": "1391" + }, + { + "label": "Formations", + "value": "1346" + }, + { + "label": "FourthDisa", + "value": "1276" + }, + { + "label": "FutureCivi", + "value": "1252" + }, + { + "label": "FamillialL", + "value": "1204" + }, + { + "label": "FemalesPro", + "value": "1178" + }, + { + "label": "Father", + "value": "1146" + }, + { + "label": "FoodShopke", + "value": "987" + }, + { + "label": "FaketoReal", + "value": "953" + }, + { + "label": "Friction", + "value": "932" + }, + { + "label": "ForcedLivi", + "value": "317" + }, + { + "label": "First-time", + "value": "192" + }, + { + "label": "Forcedinto", + "value": "110" + }, + { + "label": "Giant", + "value": "3151" + }, + { + "label": "GameElemen", + "value": "70" + }, + { + "label": "geniusflow", + "value": "3129" + }, + { + "label": "goldfinger", + "value": "1941" + }, + { + "label": "Gangster", + "value": "3226" + }, + { + "label": "Gongdou", + "value": "3254" + }, + { + "label": "gamealien", + "value": "3153" + }, + { + "label": "Greenplum", + "value": "3337" + }, + { + "label": "GeniusProt", + "value": "216" + }, + { + "label": "GroupPet", + "value": "2914" + }, + { + "label": "GetRich", + "value": "1717" + }, + { + "label": "Gods", + "value": "234" + }, + { + "label": "Grassroots", + "value": "3128" + }, + { + "label": "genius", + "value": "1421" + }, + { + "label": "Ghosts", + "value": "158" + }, + { + "label": "Group", + "value": "3188" + }, + { + "label": "Gaming", + "value": "1133" + }, + { + "label": "Gamers", + "value": "170" + }, + { + "label": "GodlyPower", + "value": "356" + }, + { + "label": "GaoWu", + "value": "5673" + }, + { + "label": "GodProtago", + "value": "298" + }, + { + "label": "GongdouZha", + "value": "5690" + }, + { + "label": "Geeky", + "value": "3408" + }, + { + "label": "Grandpa", + "value": "3180" + }, + { + "label": "General", + "value": "3349" + }, + { + "label": "Gore", + "value": "94" + }, + { + "label": "GatetoAnot", + "value": "214" + }, + { + "label": "Greatgod", + "value": "3554" + }, + { + "label": "GameRankin", + "value": "392" + }, + { + "label": "GeneticMod", + "value": "312" + }, + { + "label": "Guilds", + "value": "631" + }, + { + "label": "Groupfavor", + "value": "6151" + }, + { + "label": "Goddesses", + "value": "278" + }, + { + "label": "Generals", + "value": "233" + }, + { + "label": "Gamealienw", + "value": "6154" + }, + { + "label": "God", + "value": "1297" + }, + { + "label": "Game", + "value": "822" + }, + { + "label": "gamblingst", + "value": "3729" + }, + { + "label": "Gangs", + "value": "321" + }, + { + "label": "gene", + "value": "3315" + }, + { + "label": "groupwear", + "value": "3591" + }, + { + "label": "Guoshu", + "value": "3272" + }, + { + "label": "Gameproduc", + "value": "3154" + }, + { + "label": "Goblins", + "value": "616" + }, + { + "label": "Gunfighter", + "value": "95" + }, + { + "label": "God-levelf", + "value": "3304" + }, + { + "label": "GeneModifi", + "value": "1315" + }, + { + "label": "Grinding", + "value": "544" + }, + { + "label": "grimReaper", + "value": "3459" + }, + { + "label": "geneticwar", + "value": "3340" + }, + { + "label": "Gatevalve", + "value": "3110" + }, + { + "label": "GenderBend", + "value": "1012" + }, + { + "label": "GameElemen", + "value": "868" + }, + { + "label": "GamingE-Sp", + "value": "385" + }, + { + "label": "Guoshuflow", + "value": "5711" + }, + { + "label": "Godofcreat", + "value": "3626" + }, + { + "label": "gameplayer", + "value": "3430" + }, + { + "label": "GuardianRe", + "value": "264" + }, + { + "label": "Gotoschool", + "value": "3550" + }, + { + "label": "GameRangki", + "value": "1246" + }, + { + "label": "GoldenFing", + "value": "963" + }, + { + "label": "Grimdark", + "value": "787" + }, + { + "label": "Golems", + "value": "778" + }, + { + "label": "Gambling", + "value": "661" + }, + { + "label": "Genies", + "value": "215" + }, + { + "label": "goldenfore", + "value": "2751" + }, + { + "label": "Go", + "value": "1942" + }, + { + "label": "GenshinImp", + "value": "1321" + }, + { + "label": "growingup", + "value": "6137" + }, + { + "label": "goldenthom", + "value": "5815" + }, + { + "label": "Grayborn", + "value": "4827" + }, + { + "label": "googoogoog", + "value": "4003" + }, + { + "label": "germanicsn", + "value": "3896" + }, + { + "label": "GameofThro", + "value": "3422" + }, + { + "label": "Gundam", + "value": "1741" + }, + { + "label": "GameOnline", + "value": "1449" + }, + { + "label": "gameworld", + "value": "1220" + }, + { + "label": "Ghost", + "value": "1099" + }, + { + "label": "Gladiators", + "value": "981" + }, + { + "label": "贵女", + "value": "6180" + }, + { + "label": "Getupearly", + "value": "6126" + }, + { + "label": "glassysky", + "value": "6102" + }, + { + "label": "Getrichbys", + "value": "6072" + }, + { + "label": "Graceunder", + "value": "6007" + }, + { + "label": "Girlsareet", + "value": "5951" + }, + { + "label": "grumpyoran", + "value": "5942" + }, + { + "label": "greatbriti", + "value": "5899" + }, + { + "label": "GoneDouble", + "value": "5775" + }, + { + "label": "goldenswor", + "value": "5617" + }, + { + "label": "greenteaL", + "value": "5505" + }, + { + "label": "goodstar", + "value": "5470" + }, + { + "label": "Goo!Justki", + "value": "5465" + }, + { + "label": "gacha", + "value": "5445" + }, + { + "label": "Guluton", + "value": "5418" + }, + { + "label": "GreatCod", + "value": "5401" + }, + { + "label": "Gongdou1V1", + "value": "5334" + }, + { + "label": "goose", + "value": "5296" + }, + { + "label": "GuodianAna", + "value": "5294" + }, + { + "label": "gotta", + "value": "5247" + }, + { + "label": "GentlemanI", + "value": "5236" + }, + { + "label": "GrassCrick", + "value": "5202" + }, + { + "label": "GrilledPac", + "value": "5147" + }, + { + "label": "Gilf", + "value": "5090" + }, + { + "label": "gradually", + "value": "4994" + }, + { + "label": "gugugugu", + "value": "4913" + }, + { + "label": "Gentlygath", + "value": "4909" + }, + { + "label": "GrimReaper", + "value": "4893" + }, + { + "label": "gluttoneat", + "value": "4888" + }, + { + "label": "Gugu", + "value": "4840" + }, + { + "label": "greenfruit", + "value": "4810" + }, + { + "label": "getonit!", + "value": "4749" + }, + { + "label": "girlfallsi", + "value": "4697" + }, + { + "label": "GoldenMang", + "value": "4678" + }, + { + "label": "Granvillec", + "value": "4659" + }, + { + "label": "GentlemanM", + "value": "4646" + }, + { + "label": "God&039", + "value": "4573" + }, + { + "label": "GouTaoist", + "value": "4571" + }, + { + "label": "GlobalTop1", + "value": "4545" + }, + { + "label": "godofrainy", + "value": "4519" + }, + { + "label": "glasseswei", + "value": "4419" + }, + { + "label": "Goodsir", + "value": "4410" + }, + { + "label": "GuaguaSoar", + "value": "4406" + }, + { + "label": "greengrass", + "value": "4288" + }, + { + "label": "GambitPara", + "value": "4284" + }, + { + "label": "glasssauce", + "value": "4281" + }, + { + "label": "goldrush", + "value": "4274" + }, + { + "label": "GreatQinIm", + "value": "4263" + }, + { + "label": "GreatDrago", + "value": "4256" + }, + { + "label": "GudaoZhous", + "value": "4250" + }, + { + "label": "Godcat", + "value": "4241" + }, + { + "label": "Gouor", + "value": "4222" + }, + { + "label": "goodbyeex", + "value": "4212" + }, + { + "label": "greedydrin", + "value": "4186" + }, + { + "label": "GanYu&0", + "value": "4148" + }, + { + "label": "GreatDesol", + "value": "4147" + }, + { + "label": "GovernorWh", + "value": "4134" + }, + { + "label": "Georgia", + "value": "4034" + }, + { + "label": "GreatQinvi", + "value": "4000" + }, + { + "label": "GrilledPir", + "value": "3957" + }, + { + "label": "Grumpyoldm", + "value": "3947" + }, + { + "label": "gongregret", + "value": "3894" + }, + { + "label": "GameLit", + "value": "3763" + }, + { + "label": "GreenCong", + "value": "3577" + }, + { + "label": "GingerLemo", + "value": "3380" + }, + { + "label": "Gongdouswe", + "value": "3078" + }, + { + "label": "Goldfinger", + "value": "3031" + }, + { + "label": "geniusflow", + "value": "3016" + }, + { + "label": "gamealienp", + "value": "3006" + }, + { + "label": "gamealienO", + "value": "2978" + }, + { + "label": "gamealienV", + "value": "2966" + }, + { + "label": "Goldfinger", + "value": "2938" + }, + { + "label": "Gooifyouca", + "value": "2853" + }, + { + "label": "giantpanda", + "value": "2840" + }, + { + "label": "GeneralXie", + "value": "2773" + }, + { + "label": "Ghostexter", + "value": "2677" + }, + { + "label": "GaoYuanyao", + "value": "2666" + }, + { + "label": "goallist", + "value": "2659" + }, + { + "label": "good-natur", + "value": "2651" + }, + { + "label": "Golden", + "value": "2634" + }, + { + "label": "godsaltedf", + "value": "2589" + }, + { + "label": "GuiltyScis", + "value": "2581" + }, + { + "label": "Goddidnotg", + "value": "2414" + }, + { + "label": "Ghostsinre", + "value": "2402" + }, + { + "label": "goddessbos", + "value": "2378" + }, + { + "label": "GLL", + "value": "2329" + }, + { + "label": "Godofwings", + "value": "2324" + }, + { + "label": "GreatCeles", + "value": "2317" + }, + { + "label": "Galaxyboy", + "value": "2245" + }, + { + "label": "God&039", + "value": "2171" + }, + { + "label": "Gotaki", + "value": "2090" + }, + { + "label": "GoneStrawb", + "value": "2078" + }, + { + "label": "giveyoutim", + "value": "2058" + }, + { + "label": "gossip", + "value": "2053" + }, + { + "label": "GreatSage", + "value": "2037" + }, + { + "label": "GodofForti", + "value": "1996" + }, + { + "label": "gentleman", + "value": "1986" + }, + { + "label": "Galacticos", + "value": "1917" + }, + { + "label": "godofduel", + "value": "1908" + }, + { + "label": "goodpotdre", + "value": "1870" + }, + { + "label": "goslowbro", + "value": "1861" + }, + { + "label": "GuShaoxia", + "value": "1858" + }, + { + "label": "GradeXNUMX", + "value": "1799" + }, + { + "label": "greentea", + "value": "1777" + }, + { + "label": "GentleLove", + "value": "1750" + }, + { + "label": "GentleProt", + "value": "1722" + }, + { + "label": "Godzilla", + "value": "1710" + }, + { + "label": "Genderless", + "value": "1632" + }, + { + "label": "gameelemen", + "value": "1627" + }, + { + "label": "Government", + "value": "1616" + }, + { + "label": "Geass", + "value": "1590" + }, + { + "label": "GodlyPower", + "value": "1582" + }, + { + "label": "Goddess", + "value": "1575" + }, + { + "label": "Gourmet", + "value": "1551" + }, + { + "label": "Genshin", + "value": "1510" + }, + { + "label": "GodandDevi", + "value": "1508" + }, + { + "label": "Growth", + "value": "1486" + }, + { + "label": "Grupchat", + "value": "1477" + }, + { + "label": "GodLikeMC", + "value": "1443" + }, + { + "label": "gravityfal", + "value": "1430" + }, + { + "label": "GroupChat", + "value": "1272" + }, + { + "label": "GalaxyWars", + "value": "1266" + }, + { + "label": "GreedyProt", + "value": "1217" + }, + { + "label": "Girl&03", + "value": "1198" + }, + { + "label": "Girl&03", + "value": "1191" + }, + { + "label": "girlfriend", + "value": "1149" + }, + { + "label": "Galge", + "value": "1109" + }, + { + "label": "GraveKeepe", + "value": "1016" + }, + { + "label": "GodlyProta", + "value": "960" + }, + { + "label": "Gangsters", + "value": "798" + }, + { + "label": "Glasses-we", + "value": "703" + }, + { + "label": "God-humanR", + "value": "668" + }, + { + "label": "Glasses-we", + "value": "624" + }, + { + "label": "Genderless", + "value": "609" + }, + { + "label": "HandsomeMa", + "value": "11" + }, + { + "label": "HotBlood", + "value": "3013" + }, + { + "label": "Harem", + "value": "37" + }, + { + "label": "HE", + "value": "3152" + }, + { + "label": "housefight", + "value": "3170" + }, + { + "label": "Hikusei", + "value": "3239" + }, + { + "label": "happyenemy", + "value": "3094" + }, + { + "label": "hiddenmarr", + "value": "3258" + }, + { + "label": "Highcold", + "value": "3429" + }, + { + "label": "HidingTrue", + "value": "200" + }, + { + "label": "Heartwarmi", + "value": "106" + }, + { + "label": "HarryPotte", + "value": "237" + }, + { + "label": "HidingTrue", + "value": "117" + }, + { + "label": "HiddenAbil", + "value": "259" + }, + { + "label": "heroinecut", + "value": "3161" + }, + { + "label": "Hero", + "value": "784" + }, + { + "label": "Heroes", + "value": "536" + }, + { + "label": "HumanoidPr", + "value": "129" + }, + { + "label": "handsome", + "value": "3566" + }, + { + "label": "hunter", + "value": "3562" + }, + { + "label": "Hunters", + "value": "531" + }, + { + "label": "Hackers", + "value": "367" + }, + { + "label": "Historical", + "value": "824" + }, + { + "label": "HeavenlyTr", + "value": "128" + }, + { + "label": "Historical", + "value": "5709" + }, + { + "label": "Hacker", + "value": "957" + }, + { + "label": "HumanExper", + "value": "413" + }, + { + "label": "Humbleboy", + "value": "3277" + }, + { + "label": "horror", + "value": "818" + }, + { + "label": "HiddenGem", + "value": "982" + }, + { + "label": "History", + "value": "860" + }, + { + "label": "HonestProt", + "value": "648" + }, + { + "label": "HiddenTrue", + "value": "1107" + }, + { + "label": "HunterxHun", + "value": "1338" + }, + { + "label": "Hell", + "value": "651" + }, + { + "label": "Healers", + "value": "566" + }, + { + "label": "HardSci-fi", + "value": "3424" + }, + { + "label": "HighFantas", + "value": "1666" + }, + { + "label": "HatedProta", + "value": "112" + }, + { + "label": "highiq", + "value": "1428" + }, + { + "label": "Heaven", + "value": "582" + }, + { + "label": "HidingTrue", + "value": "1086" + }, + { + "label": "Head", + "value": "3707" + }, + { + "label": "Haveasofts", + "value": "6128" + }, + { + "label": "Hypnotism", + "value": "377" + }, + { + "label": "HiddenIden", + "value": "921" + }, + { + "label": "Handjob", + "value": "699" + }, + { + "label": "HarshTrain", + "value": "625" + }, + { + "label": "HelpfulPro", + "value": "612" + }, + { + "label": "Horor", + "value": "6160" + }, + { + "label": "Healing", + "value": "1474" + }, + { + "label": "HighSchool", + "value": "1039" + }, + { + "label": "Herbalist", + "value": "779" + }, + { + "label": "Hospital", + "value": "636" + }, + { + "label": "HumanWeapo", + "value": "490" + }, + { + "label": "婚姻", + "value": "5123" + }, + { + "label": "holymonk", + "value": "3891" + }, + { + "label": "hereIcome", + "value": "3623" + }, + { + "label": "HonkaiImpa", + "value": "1375" + }, + { + "label": "HandsomePr", + "value": "1195" + }, + { + "label": "Halo", + "value": "811" + }, + { + "label": "housegirl", + "value": "5132" + }, + { + "label": "halfnights", + "value": "4961" + }, + { + "label": "hishy", + "value": "4925" + }, + { + "label": "hibernatin", + "value": "4596" + }, + { + "label": "Hesitatean", + "value": "4126" + }, + { + "label": "HolyFireDT", + "value": "3964" + }, + { + "label": "HaotianExt", + "value": "2356" + }, + { + "label": "hey", + "value": "2094" + }, + { + "label": "Hollywood", + "value": "1196" + }, + { + "label": "HxH", + "value": "1062" + }, + { + "label": "Heterochro", + "value": "969" + }, + { + "label": "Hndjob", + "value": "376" + }, + { + "label": "焕焕", + "value": "6116" + }, + { + "label": "HolyWaterP", + "value": "6062" + }, + { + "label": "handsomebl", + "value": "6023" + }, + { + "label": "Hehehehehe", + "value": "6011" + }, + { + "label": "HuaNiaoFen", + "value": "6006" + }, + { + "label": "Hailuojun", + "value": "5988" + }, + { + "label": "HarukoHaru", + "value": "5941" + }, + { + "label": "Hongxuetyp", + "value": "5936" + }, + { + "label": "Hurry", + "value": "5914" + }, + { + "label": "headedsalt", + "value": "5911" + }, + { + "label": "Historyoft", + "value": "5885" + }, + { + "label": "hewon&0", + "value": "5855" + }, + { + "label": "HoshinoSor", + "value": "5828" + }, + { + "label": "HefanAnzi", + "value": "5805" + }, + { + "label": "humannatur", + "value": "5803" + }, + { + "label": "HongHuangB", + "value": "5779" + }, + { + "label": "HoldingaMe", + "value": "5758" + }, + { + "label": "Holdthemom", + "value": "5756" + }, + { + "label": "HyogoNorth", + "value": "5747" + }, + { + "label": "HiKeli", + "value": "5716" + }, + { + "label": "Harpertime", + "value": "5635" + }, + { + "label": "HuYuan&", + "value": "5602" + }, + { + "label": "halfstring", + "value": "5555" + }, + { + "label": "housekeepe", + "value": "5545" + }, + { + "label": "HeYeTonggu", + "value": "5523" + }, + { + "label": "hopeful", + "value": "5482" + }, + { + "label": "Huahuababy", + "value": "5444" + }, + { + "label": "harpyfrog", + "value": "5435" + }, + { + "label": "HeXueguan", + "value": "5413" + }, + { + "label": "hiddenbook", + "value": "5411" + }, + { + "label": "HalfCityDe", + "value": "5394" + }, + { + "label": "howcomfort", + "value": "5389" + }, + { + "label": "HanlinBrok", + "value": "5388" + }, + { + "label": "HERelaxeds", + "value": "5342" + }, + { + "label": "hiddenmarr", + "value": "5338" + }, + { + "label": "housefight", + "value": "5336" + }, + { + "label": "housefight", + "value": "5326" + }, + { + "label": "HongHuangz", + "value": "5286" + }, + { + "label": "HeHuaninth", + "value": "5276" + }, + { + "label": "halfsmoke", + "value": "5238" + }, + { + "label": "HolyFather", + "value": "5184" + }, + { + "label": "HansChrist", + "value": "5173" + }, + { + "label": "HunyuanDal", + "value": "5148" + }, + { + "label": "HandsomeMC", + "value": "5096" + }, + { + "label": "Humiliatio", + "value": "5093" + }, + { + "label": "HuaihaiChi", + "value": "4988" + }, + { + "label": "havealongm", + "value": "4967" + }, + { + "label": "Hidethepen", + "value": "4928" + }, + { + "label": "HeyMajesty", + "value": "4920" + }, + { + "label": "happydrago", + "value": "4886" + }, + { + "label": "horsehorse", + "value": "4873" + }, + { + "label": "HidakaDanc", + "value": "4868" + }, + { + "label": "Hardworkin", + "value": "4857" + }, + { + "label": "HamabeMiwa", + "value": "4854" + }, + { + "label": "Headmaster", + "value": "4831" + }, + { + "label": "Hotpepper", + "value": "4821" + }, + { + "label": "hugTA&0", + "value": "4704" + }, + { + "label": "hornreverb", + "value": "4691" + }, + { + "label": "human", + "value": "4653" + }, + { + "label": "HeadofShil", + "value": "4642" + }, + { + "label": "helpmeup", + "value": "4622" + }, + { + "label": "Hahariding", + "value": "4610" + }, + { + "label": "halfmoonem", + "value": "4525" + }, + { + "label": "Happyfirst", + "value": "4522" + }, + { + "label": "HonghuangS", + "value": "4496" + }, + { + "label": "Hexue", + "value": "4460" + }, + { + "label": "hardtofind", + "value": "4428" + }, + { + "label": "HongchenJi", + "value": "4387" + }, + { + "label": "hitthestre", + "value": "4385" + }, + { + "label": "HuiXiaCong", + "value": "4382" + }, + { + "label": "HisnameisJ", + "value": "4374" + }, + { + "label": "handsomeat", + "value": "4329" + }, + { + "label": "HanXiaoshe", + "value": "4318" + }, + { + "label": "heart", + "value": "4283" + }, + { + "label": "haveagoody", + "value": "4270" + }, + { + "label": "HeiTongWuG", + "value": "4268" + }, + { + "label": "HisRoyalHi", + "value": "4265" + }, + { + "label": "hunterleav", + "value": "4245" + }, + { + "label": "Honkai1999", + "value": "4171" + }, + { + "label": "Hikari", + "value": "4158" + }, + { + "label": "HengdianXi", + "value": "4146" + }, + { + "label": "honestdogg", + "value": "4144" + }, + { + "label": "haveryconf", + "value": "4095" + }, + { + "label": "hundredhou", + "value": "4064" + }, + { + "label": "Hiroyuki", + "value": "4057" + }, + { + "label": "HuaXiaowas", + "value": "4042" + }, + { + "label": "HongKongFi", + "value": "4018" + }, + { + "label": "hazelneck", + "value": "3961" + }, + { + "label": "homelessma", + "value": "3913" + }, + { + "label": "Huoshulour", + "value": "3791" + }, + { + "label": "HuoxiangZh", + "value": "3771" + }, + { + "label": "Hachime", + "value": "3688" + }, + { + "label": "happylutho", + "value": "3687" + }, + { + "label": "happylittl", + "value": "3664" + }, + { + "label": "HandjobBet", + "value": "3656" + }, + { + "label": "HalfJiangS", + "value": "3643" + }, + { + "label": "HonkaiMess", + "value": "3586" + }, + { + "label": "halfafrog", + "value": "3539" + }, + { + "label": "HeavenlyKi", + "value": "3481" + }, + { + "label": "honestking", + "value": "3470" + }, + { + "label": "HaoyuYingx", + "value": "3379" + }, + { + "label": "housefight", + "value": "3049" + }, + { + "label": "HEwearbook", + "value": "3009" + }, + { + "label": "hitten", + "value": "2927" + }, + { + "label": "Hunter×Hu", + "value": "2921" + }, + { + "label": "Hegemony", + "value": "2904" + }, + { + "label": "HappyFlow", + "value": "2825" + }, + { + "label": "Handsomegu", + "value": "2789" + }, + { + "label": "howlingwin", + "value": "2770" + }, + { + "label": "H11H", + "value": "2737" + }, + { + "label": "hotpot", + "value": "2721" + }, + { + "label": "Haminstant", + "value": "2689" + }, + { + "label": "HeartHunte", + "value": "2684" + }, + { + "label": "HonestandR", + "value": "2622" + }, + { + "label": "heartandey", + "value": "2619" + }, + { + "label": "halfanoran", + "value": "2618" + }, + { + "label": "handsomeon", + "value": "2607" + }, + { + "label": "HongfeiQin", + "value": "2597" + }, + { + "label": "heavensong", + "value": "2544" + }, + { + "label": "heavenclea", + "value": "2526" + }, + { + "label": "Haremismta", + "value": "2521" + }, + { + "label": "HonghuangN", + "value": "2516" + }, + { + "label": "halfstepge", + "value": "2476" + }, + { + "label": "HisMajesty", + "value": "2430" + }, + { + "label": "houseprope", + "value": "2412" + }, + { + "label": "HakoniwaSe", + "value": "2398" + }, + { + "label": "horrorgod", + "value": "2394" + }, + { + "label": "HuTiandi", + "value": "2386" + }, + { + "label": "HomeAttrib", + "value": "2369" + }, + { + "label": "handtearin", + "value": "2362" + }, + { + "label": "Hawkeye", + "value": "2330" + }, + { + "label": "Healthewor", + "value": "2326" + }, + { + "label": "howlingpig", + "value": "2312" + }, + { + "label": "Huijingund", + "value": "2236" + }, + { + "label": "humla", + "value": "2221" + }, + { + "label": "Honghuangs", + "value": "2217" + }, + { + "label": "酣歌", + "value": "2182" + }, + { + "label": "HappyBeanl", + "value": "2177" + }, + { + "label": "Higu", + "value": "2104" + }, + { + "label": "Hashihime", + "value": "2100" + }, + { + "label": "HuanHuanHu", + "value": "2061" + }, + { + "label": "holyangel", + "value": "2017" + }, + { + "label": "HuiMochou", + "value": "1997" + }, + { + "label": "hi", + "value": "1995" + }, + { + "label": "Heroesofth", + "value": "1961" + }, + { + "label": "hyperknigh", + "value": "1927" + }, + { + "label": "HongTang", + "value": "1886" + }, + { + "label": "hunterkill", + "value": "1881" + }, + { + "label": "HolyKingRa", + "value": "1826" + }, + { + "label": "HongmengSh", + "value": "1783" + }, + { + "label": "heroine", + "value": "1778" + }, + { + "label": "Haikyuu", + "value": "1768" + }, + { + "label": "HandsomeMa", + "value": "1656" + }, + { + "label": "Hogwarts", + "value": "1608" + }, + { + "label": "HiddenBoss", + "value": "1483" + }, + { + "label": "harrypotte", + "value": "1427" + }, + { + "label": "HumanExper", + "value": "1392" + }, + { + "label": "HiddenIden", + "value": "1216" + }, + { + "label": "hiddenvest", + "value": "1210" + }, + { + "label": "Hardworkin", + "value": "1173" + }, + { + "label": "HighSchool", + "value": "1159" + }, + { + "label": "Happy", + "value": "1157" + }, + { + "label": "HiddenYrue", + "value": "1135" + }, + { + "label": "HaremSeeki", + "value": "1084" + }, + { + "label": "Hentai", + "value": "978" + }, + { + "label": "HidingTrue", + "value": "958" + }, + { + "label": "Hotels", + "value": "906" + }, + { + "label": "Hokage", + "value": "845" + }, + { + "label": "HappyEndin", + "value": "833" + }, + { + "label": "HidingAbil", + "value": "479" + }, + { + "label": "Harem-seek", + "value": "324" + }, + { + "label": "Hot-bloode", + "value": "300" + }, + { + "label": "Half-human", + "value": "265" + }, + { + "label": "Human-Nonh", + "value": "159" + }, + { + "label": "Hard-Worki", + "value": "127" + }, + { + "label": "Invincible", + "value": "3106" + }, + { + "label": "IQOnline", + "value": "3098" + }, + { + "label": "InfiniteFl", + "value": "1292" + }, + { + "label": "Interstell", + "value": "903" + }, + { + "label": "infatuatio", + "value": "3158" + }, + { + "label": "Immortals", + "value": "235" + }, + { + "label": "Inspiratio", + "value": "3253" + }, + { + "label": "inlove", + "value": "3206" + }, + { + "label": "Imperialex", + "value": "3276" + }, + { + "label": "ideologica", + "value": "3278" + }, + { + "label": "Infrastruc", + "value": "1384" + }, + { + "label": "ImperialHa", + "value": "238" + }, + { + "label": "Industrial", + "value": "641" + }, + { + "label": "Industry", + "value": "1269" + }, + { + "label": "Incest", + "value": "179" + }, + { + "label": "ImmortalEm", + "value": "3428" + }, + { + "label": "Inheritanc", + "value": "426" + }, + { + "label": "Isekai", + "value": "850" + }, + { + "label": "Imperialco", + "value": "5706" + }, + { + "label": "Interestel", + "value": "927" + }, + { + "label": "Insects", + "value": "313" + }, + { + "label": "Investigat", + "value": "496" + }, + { + "label": "Infrastruc", + "value": "3431" + }, + { + "label": "Inferiorit", + "value": "431" + }, + { + "label": "Immortal", + "value": "1077" + }, + { + "label": "Inscriptio", + "value": "744" + }, + { + "label": "Indonesia", + "value": "1687" + }, + { + "label": "interracia", + "value": "6152" + }, + { + "label": "IdentityCr", + "value": "344" + }, + { + "label": "Interracia", + "value": "5684" + }, + { + "label": "Invincible", + "value": "5449" + }, + { + "label": "IndonesiaN", + "value": "1686" + }, + { + "label": "Industryel", + "value": "6131" + }, + { + "label": "ilovegrape", + "value": "6041" + }, + { + "label": "Iamamelanc", + "value": "4639" + }, + { + "label": "iscoding", + "value": "4211" + }, + { + "label": "indulgemyd", + "value": "4033" + }, + { + "label": "Inclined", + "value": "3970" + }, + { + "label": "IsumiLily", + "value": "2490" + }, + { + "label": "InnerVoice", + "value": "1638" + }, + { + "label": "infinite", + "value": "1368" + }, + { + "label": "Idol", + "value": "1301" + }, + { + "label": "Investigat", + "value": "1169" + }, + { + "label": "Inuyasha", + "value": "1125" + }, + { + "label": "ImperialFa", + "value": "1088" + }, + { + "label": "ImperialCu", + "value": "6117" + }, + { + "label": "Iwanttogot", + "value": "6076" + }, + { + "label": "ifthewindi", + "value": "6068" + }, + { + "label": "Iwanttohit", + "value": "6055" + }, + { + "label": "Itissaidth", + "value": "6042" + }, + { + "label": "Ireallydon", + "value": "6037" + }, + { + "label": "Idon’tkno", + "value": "6034" + }, + { + "label": "Instantnoo", + "value": "5996" + }, + { + "label": "It’sAmoyo", + "value": "5994" + }, + { + "label": "instantnoo", + "value": "5917" + }, + { + "label": "It’syouwh", + "value": "5903" + }, + { + "label": "It&039s", + "value": "5894" + }, + { + "label": "infinitega", + "value": "5825" + }, + { + "label": "Iwillbecom", + "value": "5813" + }, + { + "label": "Idiotdeado", + "value": "5777" + }, + { + "label": "Iamnottheo", + "value": "5774" + }, + { + "label": "InvolveNo.", + "value": "5740" + }, + { + "label": "interpenet", + "value": "5713" + }, + { + "label": "Itbecameaf", + "value": "5672" + }, + { + "label": "Ireallywan", + "value": "5646" + }, + { + "label": "Incarnatio", + "value": "5548" + }, + { + "label": "iwanttosee", + "value": "5513" + }, + { + "label": "IsitatLoli", + "value": "5481" + }, + { + "label": "Instructor", + "value": "5447" + }, + { + "label": "Itissaidth", + "value": "5397" + }, + { + "label": "inactionin", + "value": "5393" + }, + { + "label": "Invincible", + "value": "5244" + }, + { + "label": "Igotit!", + "value": "5201" + }, + { + "label": "it&039s", + "value": "5195" + }, + { + "label": "I&039mn", + "value": "5186" + }, + { + "label": "inchshadow", + "value": "5115" + }, + { + "label": "Imposter", + "value": "5056" + }, + { + "label": "industryel", + "value": "5053" + }, + { + "label": "ImperialCa", + "value": "5035" + }, + { + "label": "inthenameo", + "value": "4969" + }, + { + "label": "I&039ma", + "value": "4949" + }, + { + "label": "InukaiOmor", + "value": "4937" + }, + { + "label": "Itwillbead", + "value": "4932" + }, + { + "label": "Insectword", + "value": "4856" + }, + { + "label": "Iunotbad", + "value": "4834" + }, + { + "label": "idlerscome", + "value": "4788" + }, + { + "label": "ilovewatch", + "value": "4774" + }, + { + "label": "immeasurab", + "value": "4770" + }, + { + "label": "Ireallywan", + "value": "4761" + }, + { + "label": "instantsta", + "value": "4705" + }, + { + "label": "Inherenten", + "value": "4699" + }, + { + "label": "Ingeniousg", + "value": "4687" + }, + { + "label": "Iwanttosee", + "value": "4667" + }, + { + "label": "Idon&03", + "value": "4661" + }, + { + "label": "Iamareader", + "value": "4630" + }, + { + "label": "ieatguava", + "value": "4624" + }, + { + "label": "Isellflowe", + "value": "4599" + }, + { + "label": "Ink-dyedIm", + "value": "4586" + }, + { + "label": "inkdirty", + "value": "4583" + }, + { + "label": "icesweet", + "value": "4575" + }, + { + "label": "I&039mX", + "value": "4529" + }, + { + "label": "I&039mr", + "value": "4500" + }, + { + "label": "I&039mn", + "value": "4484" + }, + { + "label": "Ican&03", + "value": "4456" + }, + { + "label": "IceCreamAs", + "value": "4412" + }, + { + "label": "ihaveseven", + "value": "4407" + }, + { + "label": "Iwanttotak", + "value": "4364" + }, + { + "label": "inthecloud", + "value": "4286" + }, + { + "label": "Indestruct", + "value": "4271" + }, + { + "label": "It&039s", + "value": "4252" + }, + { + "label": "I&039ll", + "value": "4249" + }, + { + "label": "IfIdon&", + "value": "4248" + }, + { + "label": "Inexplicab", + "value": "4206" + }, + { + "label": "iceddaught", + "value": "4197" + }, + { + "label": "Inclassroo", + "value": "4169" + }, + { + "label": "Izanagi233", + "value": "4152" + }, + { + "label": "Ihaveeight", + "value": "4140" + }, + { + "label": "Icanreally", + "value": "4088" + }, + { + "label": "Idon&03", + "value": "4031" + }, + { + "label": "I&039ms", + "value": "4026" + }, + { + "label": "iamabigsal", + "value": "3963" + }, + { + "label": "IwantSiste", + "value": "3950" + }, + { + "label": "innocentmo", + "value": "3949" + }, + { + "label": "It&039s", + "value": "3932" + }, + { + "label": "insomniaun", + "value": "3869" + }, + { + "label": "IbukiGourd", + "value": "3813" + }, + { + "label": "ironfistri", + "value": "3719" + }, + { + "label": "Invincible", + "value": "3680" + }, + { + "label": "I&039mj", + "value": "3674" + }, + { + "label": "Inlovewith", + "value": "3666" + }, + { + "label": "i&039ma", + "value": "3657" + }, + { + "label": "I&039ms", + "value": "3655" + }, + { + "label": "Icomefromt", + "value": "3578" + }, + { + "label": "industrial", + "value": "3565" + }, + { + "label": "ialmostbel", + "value": "3519" + }, + { + "label": "Iloveboile", + "value": "3508" + }, + { + "label": "Ilikedried", + "value": "3495" + }, + { + "label": "Iwanttolie", + "value": "3474" + }, + { + "label": "Invincible", + "value": "3029" + }, + { + "label": "interstell", + "value": "2967" + }, + { + "label": "Invincible", + "value": "2964" + }, + { + "label": "industryHo", + "value": "2949" + }, + { + "label": "Invincible", + "value": "2942" + }, + { + "label": "Iateeightc", + "value": "2931" + }, + { + "label": "IronMaiden", + "value": "2923" + }, + { + "label": "Iwanttobea", + "value": "2866" + }, + { + "label": "IcedDurian", + "value": "2826" + }, + { + "label": "insitu", + "value": "2816" + }, + { + "label": "iwanttoeat", + "value": "2811" + }, + { + "label": "Ink", + "value": "2804" + }, + { + "label": "Intercept0", + "value": "2801" + }, + { + "label": "ImmortalMa", + "value": "2781" + }, + { + "label": "ihavethere", + "value": "2774" + }, + { + "label": "Iamthemurd", + "value": "2765" + }, + { + "label": "IamAsi", + "value": "2744" + }, + { + "label": "ilovewoo", + "value": "2704" + }, + { + "label": "Iamoldwolf", + "value": "2695" + }, + { + "label": "IronThanos", + "value": "2665" + }, + { + "label": "ImmortalBi", + "value": "2658" + }, + { + "label": "Iamtheseco", + "value": "2650" + }, + { + "label": "iwanttogot", + "value": "2636" + }, + { + "label": "ieatgrass", + "value": "2609" + }, + { + "label": "Iamtwenty-", + "value": "2596" + }, + { + "label": "Ibuprofen", + "value": "2580" + }, + { + "label": "infinitesu", + "value": "2531" + }, + { + "label": "Iwanttobeo", + "value": "2523" + }, + { + "label": "iwantmoney", + "value": "2463" + }, + { + "label": "Iamhell", + "value": "2443" + }, + { + "label": "IamGuanxi", + "value": "2438" + }, + { + "label": "I&039mo", + "value": "2351" + }, + { + "label": "ironpillar", + "value": "2339" + }, + { + "label": "Iamapirate", + "value": "2321" + }, + { + "label": "Iamfifth", + "value": "2314" + }, + { + "label": "Iamarealdi", + "value": "2304" + }, + { + "label": "IamHisMaje", + "value": "2296" + }, + { + "label": "Iamolderth", + "value": "2269" + }, + { + "label": "idon&03", + "value": "2195" + }, + { + "label": "I&039ma", + "value": "2191" + }, + { + "label": "InfiniteBu", + "value": "2190" + }, + { + "label": "It&039s", + "value": "2179" + }, + { + "label": "Ifyoucango", + "value": "2118" + }, + { + "label": "Iwishyouat", + "value": "2114" + }, + { + "label": "Itsdaybrea", + "value": "2113" + }, + { + "label": "Isuckbrown", + "value": "2102" + }, + { + "label": "Infernalco", + "value": "2067" + }, + { + "label": "isitnecess", + "value": "2063" + }, + { + "label": "icalledthe", + "value": "2036" + }, + { + "label": "Infiniteme", + "value": "2027" + }, + { + "label": "Iliveupsta", + "value": "2010" + }, + { + "label": "Iamnotaloc", + "value": "2009" + }, + { + "label": "Intercept0", + "value": "1988" + }, + { + "label": "icewalk", + "value": "1979" + }, + { + "label": "Invincible", + "value": "1956" + }, + { + "label": "iamatravel", + "value": "1930" + }, + { + "label": "Ijustwantt", + "value": "1906" + }, + { + "label": "Iamtheseak", + "value": "1890" + }, + { + "label": "Invincible", + "value": "1887" + }, + { + "label": "Invincible", + "value": "1875" + }, + { + "label": "idropbaby", + "value": "1839" + }, + { + "label": "Invincible", + "value": "1812" + }, + { + "label": "Ilo", + "value": "1786" + }, + { + "label": "Interconne", + "value": "1745" + }, + { + "label": "Illigitima", + "value": "1714" + }, + { + "label": "IsItWrongt", + "value": "1671" + }, + { + "label": "imperialco", + "value": "1473" + }, + { + "label": "infrastrac", + "value": "1374" + }, + { + "label": "intenseflu", + "value": "1029" + }, + { + "label": "Incubus", + "value": "1019" + }, + { + "label": "Introverte", + "value": "680" + }, + { + "label": "Indecisive", + "value": "568" + }, + { + "label": "Interdimen", + "value": "27" + }, + { + "label": "Japanese", + "value": "843" + }, + { + "label": "Jianghugri", + "value": "5697" + }, + { + "label": "Jealousy", + "value": "258" + }, + { + "label": "JackofAllT", + "value": "71" + }, + { + "label": "家族", + "value": "3460" + }, + { + "label": "JianBao", + "value": "3209" + }, + { + "label": "氪金", + "value": "3312" + }, + { + "label": "金融", + "value": "3101" + }, + { + "label": "JiDaoliu", + "value": "6170" + }, + { + "label": "Journeytot", + "value": "1326" + }, + { + "label": "教授", + "value": "3777" + }, + { + "label": "JinYiwei", + "value": "3400" + }, + { + "label": "JujutsuKai", + "value": "1127" + }, + { + "label": "Josei", + "value": "886" + }, + { + "label": "justdrunk", + "value": "5245" + }, + { + "label": "Jiangshi", + "value": "603" + }, + { + "label": "JingShenTi", + "value": "5879" + }, + { + "label": "jediescape", + "value": "5862" + }, + { + "label": "JasperKnif", + "value": "5564" + }, + { + "label": "JiangWu", + "value": "4841" + }, + { + "label": "JaneShu", + "value": "3806" + }, + { + "label": "JoJo", + "value": "2882" + }, + { + "label": "Jazz", + "value": "2679" + }, + { + "label": "Jin", + "value": "6176" + }, + { + "label": "JiYipao", + "value": "6004" + }, + { + "label": "Jiulu丶", + "value": "5846" + }, + { + "label": "July&03", + "value": "5831" + }, + { + "label": "jzkyushu", + "value": "5808" + }, + { + "label": "Jundrunkdr", + "value": "5770" + }, + { + "label": "Jung", + "value": "5608" + }, + { + "label": "JoanHuang", + "value": "5516" + }, + { + "label": "Jackdaw", + "value": "5382" + }, + { + "label": "JiangYu", + "value": "5034" + }, + { + "label": "JixingGaoz", + "value": "4989" + }, + { + "label": "justplay", + "value": "4879" + }, + { + "label": "JiangCheng", + "value": "4785" + }, + { + "label": "justsaydon", + "value": "4685" + }, + { + "label": "JunRuoxu", + "value": "4605" + }, + { + "label": "JinguNoFun", + "value": "4570" + }, + { + "label": "JunRuyu", + "value": "4422" + }, + { + "label": "JulyBrewma", + "value": "4313" + }, + { + "label": "JKing", + "value": "4205" + }, + { + "label": "Jiaqi", + "value": "4173" + }, + { + "label": "Jiuxiao", + "value": "4166" + }, + { + "label": "JOAmbulanc", + "value": "4065" + }, + { + "label": "justapiece", + "value": "3988" + }, + { + "label": "januarygod", + "value": "3953" + }, + { + "label": "JiuYuan", + "value": "3737" + }, + { + "label": "Jinwan1", + "value": "3499" + }, + { + "label": "Jaderabbit", + "value": "3463" + }, + { + "label": "JackieChan", + "value": "2891" + }, + { + "label": "JoJo&03", + "value": "2883" + }, + { + "label": "jellyjelly", + "value": "2872" + }, + { + "label": "justshout", + "value": "2795" + }, + { + "label": "JunCaiXing", + "value": "2791" + }, + { + "label": "JinglongTa", + "value": "2763" + }, + { + "label": "John117", + "value": "2641" + }, + { + "label": "Jianjiamix", + "value": "2387" + }, + { + "label": "jadeeveryy", + "value": "2334" + }, + { + "label": "JuniorSist", + "value": "2176" + }, + { + "label": "Jiutianyu", + "value": "2170" + }, + { + "label": "joydrummer", + "value": "2013" + }, + { + "label": "JOJOWE", + "value": "1954" + }, + { + "label": "Juliet", + "value": "1811" + }, + { + "label": "JangSeok-g", + "value": "1793" + }, + { + "label": "Japan", + "value": "1342" + }, + { + "label": "Jianghu", + "value": "1258" + }, + { + "label": "JojoBizarr", + "value": "1126" + }, + { + "label": "JapIdols", + "value": "907" + }, + { + "label": "killdecisi", + "value": "3126" + }, + { + "label": "空间", + "value": "3103" + }, + { + "label": "Korean", + "value": "1674" + }, + { + "label": "KingdomBui", + "value": "340" + }, + { + "label": "KingofSold", + "value": "3268" + }, + { + "label": "Koi", + "value": "1578" + }, + { + "label": "Kingdoms", + "value": "181" + }, + { + "label": "KoreanNove", + "value": "1310" + }, + { + "label": "Knight", + "value": "900" + }, + { + "label": "King", + "value": "1901" + }, + { + "label": "Knights", + "value": "208" + }, + { + "label": "Killer", + "value": "3608" + }, + { + "label": "Kindness", + "value": "3405" + }, + { + "label": "Kingdom-bu", + "value": "871" + }, + { + "label": "KindLoveIn", + "value": "131" + }, + { + "label": "Kidnapping", + "value": "509" + }, + { + "label": "knightflow", + "value": "5707" + }, + { + "label": "Kuudere", + "value": "708" + }, + { + "label": "KnightsLev", + "value": "613" + }, + { + "label": "KindProtag", + "value": "1464" + }, + { + "label": "KpopIdols", + "value": "908" + }, + { + "label": "KingdomsKn", + "value": "533" + }, + { + "label": "Kaying", + "value": "4258" + }, + { + "label": "KnowKingAl", + "value": "3815" + }, + { + "label": "Kojin", + "value": "2216" + }, + { + "label": "Kindergart", + "value": "1754" + }, + { + "label": "开凡", + "value": "6111" + }, + { + "label": "killerwhal", + "value": "6086" + }, + { + "label": "KimuraShit", + "value": "6067" + }, + { + "label": "Kyokowassi", + "value": "5812" + }, + { + "label": "killpigeon", + "value": "5761" + }, + { + "label": "KiharaKaqu", + "value": "5587" + }, + { + "label": "knightinar", + "value": "5580" + }, + { + "label": "Knockingca", + "value": "5566" + }, + { + "label": "KennelSaku", + "value": "5558" + }, + { + "label": "Kuchikiwha", + "value": "5538" + }, + { + "label": "KotomineSa", + "value": "5531" + }, + { + "label": "Kahn", + "value": "5387" + }, + { + "label": "kittenrun", + "value": "5223" + }, + { + "label": "KunHao", + "value": "5149" + }, + { + "label": "kingoffoot", + "value": "5111" + }, + { + "label": "Katena", + "value": "5012" + }, + { + "label": "KensenShin", + "value": "4795" + }, + { + "label": "KakamieCro", + "value": "4666" + }, + { + "label": "Karmadance", + "value": "4604" + }, + { + "label": "knightmeow", + "value": "4497" + }, + { + "label": "knowfate", + "value": "4461" + }, + { + "label": "KanohiroSa", + "value": "4261" + }, + { + "label": "keepitsimp", + "value": "4254" + }, + { + "label": "knightcomm", + "value": "4231" + }, + { + "label": "keel", + "value": "4195" + }, + { + "label": "Kutiaoren", + "value": "4170" + }, + { + "label": "Konohasalt", + "value": "4058" + }, + { + "label": "KakashiTen", + "value": "4023" + }, + { + "label": "keykey", + "value": "3958" + }, + { + "label": "Kailandros", + "value": "3886" + }, + { + "label": "Kim&039", + "value": "3879" + }, + { + "label": "KyokoKurot", + "value": "3807" + }, + { + "label": "keyboardki", + "value": "3785" + }, + { + "label": "KoiKing", + "value": "3757" + }, + { + "label": "Kindergart", + "value": "3726" + }, + { + "label": "knightupgr", + "value": "3621" + }, + { + "label": "Kazamahyac", + "value": "3538" + }, + { + "label": "KingLingyu", + "value": "3528" + }, + { + "label": "Kneelingan", + "value": "2796" + }, + { + "label": "KingKonggo", + "value": "2629" + }, + { + "label": "kingofdeat", + "value": "2592" + }, + { + "label": "KonohaVoll", + "value": "2532" + }, + { + "label": "Kiritani", + "value": "2518" + }, + { + "label": "Knowtheric", + "value": "2478" + }, + { + "label": "keytocome", + "value": "2458" + }, + { + "label": "KingofMons", + "value": "2437" + }, + { + "label": "KingAsura", + "value": "2391" + }, + { + "label": "Kneelingth", + "value": "2305" + }, + { + "label": "Killthewor", + "value": "2196" + }, + { + "label": "kingpirate", + "value": "2185" + }, + { + "label": "KwunTong", + "value": "2043" + }, + { + "label": "Kafkajumpi", + "value": "2035" + }, + { + "label": "KurongTemp", + "value": "2008" + }, + { + "label": "KnifePromi", + "value": "2001" + }, + { + "label": "KingofDest", + "value": "1823" + }, + { + "label": "Kingdom", + "value": "1629" + }, + { + "label": "KindomBuil", + "value": "1462" + }, + { + "label": "K-popIdols", + "value": "1102" + }, + { + "label": "Kendo", + "value": "1001" + }, + { + "label": "Karma", + "value": "999" + }, + { + "label": "Kakashi", + "value": "846" + }, + { + "label": "Love", + "value": "1498" + }, + { + "label": "LightNovel", + "value": "1680" + }, + { + "label": "lovebefore", + "value": "3217" + }, + { + "label": "Lol", + "value": "3139" + }, + { + "label": "LoveatFirs", + "value": "168" + }, + { + "label": "Loveeachot", + "value": "3127" + }, + { + "label": "LevelSyste", + "value": "201" + }, + { + "label": "lady", + "value": "3403" + }, + { + "label": "Livetext", + "value": "3136" + }, + { + "label": "LuckyProta", + "value": "368" + }, + { + "label": "luckybag", + "value": "3240" + }, + { + "label": "layoutflow", + "value": "3192" + }, + { + "label": "loyaldog", + "value": "3305" + }, + { + "label": "Loli", + "value": "576" + }, + { + "label": "LoyalSubor", + "value": "507" + }, + { + "label": "LateRomanc", + "value": "249" + }, + { + "label": "Livebroadc", + "value": "919" + }, + { + "label": "LitRPG", + "value": "983" + }, + { + "label": "LiveStream", + "value": "1604" + }, + { + "label": "leadthemai", + "value": "3357" + }, + { + "label": "LazyProtag", + "value": "441" + }, + { + "label": "Leadership", + "value": "375" + }, + { + "label": "LackofComm", + "value": "160" + }, + { + "label": "lootflow", + "value": "3333" + }, + { + "label": "Lordfarmin", + "value": "3410" + }, + { + "label": "Low-keyPro", + "value": "328" + }, + { + "label": "Longevity", + "value": "3299" + }, + { + "label": "lifeanddea", + "value": "3568" + }, + { + "label": "Lolicon", + "value": "425" + }, + { + "label": "lastwear", + "value": "3595" + }, + { + "label": "Lingen", + "value": "3332" + }, + { + "label": "Lottery", + "value": "573" + }, + { + "label": "loser", + "value": "5691" + }, + { + "label": "Leftgirl", + "value": "3612" + }, + { + "label": "Long", + "value": "3632" + }, + { + "label": "lawyer", + "value": "3458" + }, + { + "label": "LiaoZhai", + "value": "3445" + }, + { + "label": "LongSepara", + "value": "412" + }, + { + "label": "lowintelli", + "value": "3703" + }, + { + "label": "LoveTriang", + "value": "485" + }, + { + "label": "Levelup", + "value": "5055" + }, + { + "label": "LonerProta", + "value": "585" + }, + { + "label": "LordGod", + "value": "3705" + }, + { + "label": "Littlesold", + "value": "3263" + }, + { + "label": "LoveRivals", + "value": "610" + }, + { + "label": "Lawyers", + "value": "548" + }, + { + "label": "LoversReun", + "value": "502" + }, + { + "label": "Lifestyle", + "value": "5678" + }, + { + "label": "LowkeyProt", + "value": "1182" + }, + { + "label": "LordoftheM", + "value": "895" + }, + { + "label": "Legends", + "value": "607" + }, + { + "label": "Lovecomedy", + "value": "4510" + }, + { + "label": "ListCreati", + "value": "1335" + }, + { + "label": "LiveStream", + "value": "883" + }, + { + "label": "LivingAlon", + "value": "474" + }, + { + "label": "LiuZiqing", + "value": "3936" + }, + { + "label": "Lillie", + "value": "2642" + }, + { + "label": "LoveContra", + "value": "1576" + }, + { + "label": "LimitedLif", + "value": "470" + }, + { + "label": "LinQinghua", + "value": "4189" + }, + { + "label": "LordGrim", + "value": "3722" + }, + { + "label": "literature", + "value": "1416" + }, + { + "label": "Literaryes", + "value": "5692" + }, + { + "label": "lifeextrac", + "value": "5023" + }, + { + "label": "lookingout", + "value": "4615" + }, + { + "label": "littlebow", + "value": "4542" + }, + { + "label": "lightfluff", + "value": "4503" + }, + { + "label": "longsongan", + "value": "4275" + }, + { + "label": "LiZhitaoJu", + "value": "4111" + }, + { + "label": "LoadingApp", + "value": "4056" + }, + { + "label": "LazyYang", + "value": "4016" + }, + { + "label": "lightsaber", + "value": "3682" + }, + { + "label": "LingshanIs", + "value": "2654" + }, + { + "label": "Lovesnacks", + "value": "2183" + }, + { + "label": "LaoLaoXu", + "value": "2076" + }, + { + "label": "LuoWei", + "value": "1994" + }, + { + "label": "LiWudi", + "value": "1877" + }, + { + "label": "LordAbilit", + "value": "1724" + }, + { + "label": "LifeScript", + "value": "1721" + }, + { + "label": "LuckPlunde", + "value": "1720" + }, + { + "label": "LowFantasy", + "value": "1649" + }, + { + "label": "Luck", + "value": "1579" + }, + { + "label": "LGBTQA", + "value": "994" + }, + { + "label": "Library", + "value": "773" + }, + { + "label": "LostCivili", + "value": "330" + }, + { + "label": "LoveintheR", + "value": "6140" + }, + { + "label": "LiMengxi", + "value": "6106" + }, + { + "label": "LinShendu", + "value": "6103" + }, + { + "label": "Littlewood", + "value": "6096" + }, + { + "label": "Lifelongfa", + "value": "6094" + }, + { + "label": "littlelitt", + "value": "6080" + }, + { + "label": "LuoLiisnot", + "value": "6052" + }, + { + "label": "Lookingbac", + "value": "6020" + }, + { + "label": "Lovetosuck", + "value": "5991" + }, + { + "label": "Luoran123", + "value": "5990" + }, + { + "label": "LinYiliu", + "value": "5970" + }, + { + "label": "lionazaaza", + "value": "5968" + }, + { + "label": "LazycatZ", + "value": "5958" + }, + { + "label": "Listentoth", + "value": "5956" + }, + { + "label": "Lemonade", + "value": "5881" + }, + { + "label": "Lonely", + "value": "5856" + }, + { + "label": "lonelyseas", + "value": "5849" + }, + { + "label": "Let&039", + "value": "5845" + }, + { + "label": "LambSoupwi", + "value": "5839" + }, + { + "label": "listentoth", + "value": "5836" + }, + { + "label": "LiYuer", + "value": "5786" + }, + { + "label": "LingofGemi", + "value": "5785" + }, + { + "label": "laughandcr", + "value": "5780" + }, + { + "label": "LinYouyu", + "value": "5757" + }, + { + "label": "LiuYuxuemo", + "value": "5749" + }, + { + "label": "littleassa", + "value": "5746" + }, + { + "label": "longliveth", + "value": "5733" + }, + { + "label": "Leavingaga", + "value": "5725" + }, + { + "label": "Littlefurb", + "value": "5670" + }, + { + "label": "latenightf", + "value": "5664" + }, + { + "label": "LongShao12", + "value": "5655" + }, + { + "label": "LittleDoud", + "value": "5644" + }, + { + "label": "LordCrimso", + "value": "5641" + }, + { + "label": "LiuXiaozu", + "value": "5613" + }, + { + "label": "LoveVegeta", + "value": "5600" + }, + { + "label": "Liedown", + "value": "5547" + }, + { + "label": "lindenone", + "value": "5528" + }, + { + "label": "Leaningont", + "value": "5507" + }, + { + "label": "Liuhuaacri", + "value": "5496" + }, + { + "label": "LuoShichao", + "value": "5493" + }, + { + "label": "LuoFeige", + "value": "5473" + }, + { + "label": "lonewolfl", + "value": "5471" + }, + { + "label": "leavesfall", + "value": "5467" + }, + { + "label": "littleflow", + "value": "5461" + }, + { + "label": "LinSake", + "value": "5460" + }, + { + "label": "leftpigeon", + "value": "5457" + }, + { + "label": "Lily&03", + "value": "5440" + }, + { + "label": "LiXiaojian", + "value": "5431" + }, + { + "label": "Leaves", + "value": "5409" + }, + { + "label": "LawofNineG", + "value": "5403" + }, + { + "label": "Longtimeco", + "value": "5399" + }, + { + "label": "Littleblue", + "value": "5390" + }, + { + "label": "lovebefore", + "value": "5332" + }, + { + "label": "lifealive2", + "value": "5301" + }, + { + "label": "LingLingSa", + "value": "5291" + }, + { + "label": "lsuyangl", + "value": "5285" + }, + { + "label": "lostblack", + "value": "5260" + }, + { + "label": "LittleTeem", + "value": "5242" + }, + { + "label": "LittleFeng", + "value": "5237" + }, + { + "label": "LuoXiaoqia", + "value": "5232" + }, + { + "label": "Lostfarewe", + "value": "5208" + }, + { + "label": "lspstoryte", + "value": "5180" + }, + { + "label": "losemoney", + "value": "5166" + }, + { + "label": "LilyNo.XNU", + "value": "5150" + }, + { + "label": "lasercatbo", + "value": "5137" + }, + { + "label": "LittleMo", + "value": "5109" + }, + { + "label": "LiYouqiong", + "value": "5100" + }, + { + "label": "LingwuLuqi", + "value": "5042" + }, + { + "label": "LittleRedR", + "value": "5024" + }, + { + "label": "LongYu", + "value": "5000" + }, + { + "label": "LiyuePlann", + "value": "4998" + }, + { + "label": "LangyaPavi", + "value": "4992" + }, + { + "label": "Lifeisthre", + "value": "4975" + }, + { + "label": "Let&039", + "value": "4898" + }, + { + "label": "lovetoeatr", + "value": "4870" + }, + { + "label": "Lostbeliev", + "value": "4858" + }, + { + "label": "lonelyshoo", + "value": "4798" + }, + { + "label": "Littledemo", + "value": "4797" + }, + { + "label": "Lianhai&am", + "value": "4794" + }, + { + "label": "LuoJiangsh", + "value": "4791" + }, + { + "label": "LiBaiisnot", + "value": "4779" + }, + { + "label": "LianHongyu", + "value": "4767" + }, + { + "label": "lazycatsch", + "value": "4720" + }, + { + "label": "LargeBoard", + "value": "4717" + }, + { + "label": "Lappy", + "value": "4715" + }, + { + "label": "Lungdefici", + "value": "4713" + }, + { + "label": "Longanloli", + "value": "4708" + }, + { + "label": "Longpigeon", + "value": "4688" + }, + { + "label": "loveapplep", + "value": "4665" + }, + { + "label": "LingLuoBai", + "value": "4663" + }, + { + "label": "Lookthroug", + "value": "4649" + }, + { + "label": "littlefoxi", + "value": "4645" + }, + { + "label": "lovespicyf", + "value": "4643" + }, + { + "label": "littlewhit", + "value": "4638" + }, + { + "label": "LuoAichen", + "value": "4633" + }, + { + "label": "LordoftheS", + "value": "4618" + }, + { + "label": "LeafofTian", + "value": "4590" + }, + { + "label": "littlemiss", + "value": "4588" + }, + { + "label": "LosAngeles", + "value": "4576" + }, + { + "label": "LengquanAt", + "value": "4549" + }, + { + "label": "littlewhit", + "value": "4531" + }, + { + "label": "lonelyreco", + "value": "4505" + }, + { + "label": "Longcenter", + "value": "4475" + }, + { + "label": "littleches", + "value": "4474" + }, + { + "label": "lazyangel", + "value": "4469" + }, + { + "label": "Lookatthep", + "value": "4464" + }, + { + "label": "loveletter", + "value": "4462" + }, + { + "label": "Longmensin", + "value": "4423" + }, + { + "label": "lovemints", + "value": "4413" + }, + { + "label": "lazyandcru", + "value": "4399" + }, + { + "label": "LingHanyi", + "value": "4384" + }, + { + "label": "longnightl", + "value": "4380" + }, + { + "label": "luxurypenm", + "value": "4378" + }, + { + "label": "LeJiangli", + "value": "4331" + }, + { + "label": "LianshanGu", + "value": "4323" + }, + { + "label": "LutherHark", + "value": "4322" + }, + { + "label": "Leaningont", + "value": "4312" + }, + { + "label": "LiShaoming", + "value": "4309" + }, + { + "label": "lilac", + "value": "4307" + }, + { + "label": "LuoMuziyi", + "value": "4302" + }, + { + "label": "lightsail", + "value": "4299" + }, + { + "label": "Leavingthe", + "value": "4282" + }, + { + "label": "Levatin", + "value": "4243" + }, + { + "label": "LittleThro", + "value": "4239" + }, + { + "label": "LujingVill", + "value": "4209" + }, + { + "label": "LordofWar1", + "value": "4207" + }, + { + "label": "LiveGemini", + "value": "4191" + }, + { + "label": "leggod", + "value": "4190" + }, + { + "label": "lemoneatar", + "value": "4178" + }, + { + "label": "LittleSpri", + "value": "4175" + }, + { + "label": "levelthree", + "value": "4156" + }, + { + "label": "listentoth", + "value": "4141" + }, + { + "label": "LittleJack", + "value": "4083" + }, + { + "label": "lastnight", + "value": "4072" + }, + { + "label": "LuckyE&", + "value": "4071" + }, + { + "label": "littleworl", + "value": "4069" + }, + { + "label": "lazycaramb", + "value": "4062" + }, + { + "label": "Littlepros", + "value": "4044" + }, + { + "label": "Listentoth", + "value": "4032" + }, + { + "label": "Lingxiwhow", + "value": "4021" + }, + { + "label": "littledoct", + "value": "3984" + }, + { + "label": "LaoWang", + "value": "3976" + }, + { + "label": "laughingor", + "value": "3954" + }, + { + "label": "LittleZhug", + "value": "3939" + }, + { + "label": "littleclou", + "value": "3920" + }, + { + "label": "lessmeatmo", + "value": "3918" + }, + { + "label": "LinSanjiu", + "value": "3900" + }, + { + "label": "LiangFeifa", + "value": "3888" + }, + { + "label": "LuoShu", + "value": "3887" + }, + { + "label": "LeYang", + "value": "3883" + }, + { + "label": "LingyueRed", + "value": "3882" + }, + { + "label": "lemonlemon", + "value": "3875" + }, + { + "label": "Lemonfruit", + "value": "3870" + }, + { + "label": "LordofDark", + "value": "3868" + }, + { + "label": "LongquanNo", + "value": "3863" + }, + { + "label": "LongStrip", + "value": "3843" + }, + { + "label": "ListAdvent", + "value": "3842" + }, + { + "label": "LaidBack", + "value": "3821" + }, + { + "label": "Lorddevelo", + "value": "3819" + }, + { + "label": "leadwall", + "value": "3801" + }, + { + "label": "LuoTianyic", + "value": "3788" + }, + { + "label": "leftandrig", + "value": "3746" + }, + { + "label": "Literature", + "value": "3695" + }, + { + "label": "Letmetelly", + "value": "3692" + }, + { + "label": "LeiHuofeng", + "value": "3670" + }, + { + "label": "Luckytolau", + "value": "3650" + }, + { + "label": "LittleWhit", + "value": "3596" + }, + { + "label": "lazy", + "value": "3555" + }, + { + "label": "leaningont", + "value": "3542" + }, + { + "label": "LonelyChen", + "value": "3532" + }, + { + "label": "lightandsh", + "value": "3526" + }, + { + "label": "Lazyintoab", + "value": "3525" + }, + { + "label": "listentoth", + "value": "3524" + }, + { + "label": "LingbiWhit", + "value": "3518" + }, + { + "label": "lifestillh", + "value": "3509" + }, + { + "label": "lightofday", + "value": "3492" + }, + { + "label": "LaiXiaomin", + "value": "3482" + }, + { + "label": "lumpofjade", + "value": "3468" + }, + { + "label": "LeiXunqing", + "value": "3372" + }, + { + "label": "loveatfirs", + "value": "3087" + }, + { + "label": "lovebefore", + "value": "3024" + }, + { + "label": "layoutflow", + "value": "2982" + }, + { + "label": "零充", + "value": "2924" + }, + { + "label": "longlivedm", + "value": "2877" + }, + { + "label": "lonelyboy", + "value": "2855" + }, + { + "label": "littlewate", + "value": "2848" + }, + { + "label": "lendmefive", + "value": "2837" + }, + { + "label": "littlecray", + "value": "2823" + }, + { + "label": "Low-keylux", + "value": "2818" + }, + { + "label": "littledete", + "value": "2803" + }, + { + "label": "lovetease", + "value": "2785" + }, + { + "label": "LiJunhao", + "value": "2756" + }, + { + "label": "LuoYuqianq", + "value": "2752" + }, + { + "label": "Lindentree", + "value": "2745" + }, + { + "label": "lu11034363", + "value": "2734" + }, + { + "label": "LinBei", + "value": "2730" + }, + { + "label": "listentoth", + "value": "2717" + }, + { + "label": "LinXiufigh", + "value": "2668" + }, + { + "label": "Longliveth", + "value": "2655" + }, + { + "label": "LordTiansh", + "value": "2614" + }, + { + "label": "LikeMeiAox", + "value": "2582" + }, + { + "label": "LinZhengyi", + "value": "2572" + }, + { + "label": "littlewind", + "value": "2558" + }, + { + "label": "Longlivesa", + "value": "2556" + }, + { + "label": "LuoXIV", + "value": "2530" + }, + { + "label": "laborhonor", + "value": "2527" + }, + { + "label": "LY", + "value": "2474" + }, + { + "label": "longlivemy", + "value": "2456" + }, + { + "label": "langyalist", + "value": "2441" + }, + { + "label": "Longliveth", + "value": "2439" + }, + { + "label": "Leisurelys", + "value": "2426" + }, + { + "label": "LiuYujun", + "value": "2418" + }, + { + "label": "Longpigeon", + "value": "2390" + }, + { + "label": "littlefing", + "value": "2388" + }, + { + "label": "littledevi", + "value": "2357" + }, + { + "label": "Leapeveryd", + "value": "2347" + }, + { + "label": "LordZhangj", + "value": "2332" + }, + { + "label": "littlesuns", + "value": "2320" + }, + { + "label": "LameHaoisa", + "value": "2310" + }, + { + "label": "littlebrot", + "value": "2291" + }, + { + "label": "LongMengme", + "value": "2287" + }, + { + "label": "lemonandsi", + "value": "2285" + }, + { + "label": "LiMumu", + "value": "2280" + }, + { + "label": "lackofboat", + "value": "2262" + }, + { + "label": "LoneCloudP", + "value": "2242" + }, + { + "label": "laurel", + "value": "2201" + }, + { + "label": "littleahxi", + "value": "2197" + }, + { + "label": "Lightnings", + "value": "2163" + }, + { + "label": "littlefox", + "value": "2152" + }, + { + "label": "littledemo", + "value": "2130" + }, + { + "label": "LuDehua", + "value": "2125" + }, + { + "label": "lazydevil", + "value": "2120" + }, + { + "label": "LacquerNig", + "value": "2099" + }, + { + "label": "Lingran", + "value": "2089" + }, + { + "label": "littlemoon", + "value": "2041" + }, + { + "label": "LiverPigeo", + "value": "2033" + }, + { + "label": "littlezlov", + "value": "2031" + }, + { + "label": "LonelyCity", + "value": "2007" + }, + { + "label": "littlehson", + "value": "2002" + }, + { + "label": "Lonelynota", + "value": "1990" + }, + { + "label": "LordoftheS", + "value": "1976" + }, + { + "label": "LeiJiedoes", + "value": "1933" + }, + { + "label": "Loseafewpo", + "value": "1925" + }, + { + "label": "LikeaDrago", + "value": "1907" + }, + { + "label": "LikeaDrago", + "value": "1902" + }, + { + "label": "LuoTianyi", + "value": "1865" + }, + { + "label": "lesstime", + "value": "1842" + }, + { + "label": "LoveIntere", + "value": "1660" + }, + { + "label": "LoveIntere", + "value": "1624" + }, + { + "label": "LoyalSurbo", + "value": "1599" + }, + { + "label": "LordGodSpa", + "value": "1588" + }, + { + "label": "LoliProtag", + "value": "1518" + }, + { + "label": "lgbt", + "value": "1429" + }, + { + "label": "lightnovel", + "value": "1405" + }, + { + "label": "lovingfami", + "value": "1397" + }, + { + "label": "LoveandMar", + "value": "1385" + }, + { + "label": "LoyalProta", + "value": "1289" + }, + { + "label": "Liar", + "value": "1165" + }, + { + "label": "LowKeyMc", + "value": "1090" + }, + { + "label": "leonine", + "value": "1035" + }, + { + "label": "LittleRoma", + "value": "990" + }, + { + "label": "LiveBroadc", + "value": "941" + }, + { + "label": "Luxury", + "value": "909" + }, + { + "label": "LeagueofLe", + "value": "849" + }, + { + "label": "LotterySys", + "value": "817" + }, + { + "label": "LimitlessF", + "value": "794" + }, + { + "label": "Legend", + "value": "701" + }, + { + "label": "Long-dista", + "value": "273" + }, + { + "label": "LanguageBa", + "value": "133" + }, + { + "label": "LoveIntere", + "value": "19" + }, + { + "label": "MaleProtag", + "value": "12" + }, + { + "label": "Magic", + "value": "182" + }, + { + "label": "ModernDay", + "value": "3" + }, + { + "label": "Mature", + "value": "977" + }, + { + "label": "MengBao", + "value": "3176" + }, + { + "label": "Manage", + "value": "3091" + }, + { + "label": "Monsters", + "value": "230" + }, + { + "label": "metaphysic", + "value": "3234" + }, + { + "label": "Makemoney", + "value": "3099" + }, + { + "label": "Martialart", + "value": "130" + }, + { + "label": "Marvel", + "value": "537" + }, + { + "label": "Military", + "value": "86" + }, + { + "label": "Misunderst", + "value": "1445" + }, + { + "label": "Marriage", + "value": "85" + }, + { + "label": "Malegod", + "value": "3118" + }, + { + "label": "mortalflow", + "value": "3150" + }, + { + "label": "Misunderst", + "value": "113" + }, + { + "label": "MultipleRe", + "value": "29" + }, + { + "label": "MingDynast", + "value": "3089" + }, + { + "label": "MagicalSpa", + "value": "73" + }, + { + "label": "ModernKnow", + "value": "307" + }, + { + "label": "mouthgun", + "value": "3167" + }, + { + "label": "marrybycha", + "value": "3218" + }, + { + "label": "Mage", + "value": "1013" + }, + { + "label": "ModernWorl", + "value": "386" + }, + { + "label": "Makecompla", + "value": "3242" + }, + { + "label": "MultiplePO", + "value": "279" + }, + { + "label": "MedicalKno", + "value": "299" + }, + { + "label": "Mythology", + "value": "484" + }, + { + "label": "MMORPG", + "value": "202" + }, + { + "label": "Modern", + "value": "601" + }, + { + "label": "MagicBeast", + "value": "314" + }, + { + "label": "moderncomp", + "value": "3319" + }, + { + "label": "Mech", + "value": "3289" + }, + { + "label": "Mentor", + "value": "3191" + }, + { + "label": "miser", + "value": "3140" + }, + { + "label": "Mpreg", + "value": "257" + }, + { + "label": "ModernFant", + "value": "1027" + }, + { + "label": "modernmyst", + "value": "3413" + }, + { + "label": "Mensao", + "value": "3322" + }, + { + "label": "MonsterTam", + "value": "74" + }, + { + "label": "Mysterious", + "value": "388" + }, + { + "label": "MaleYander", + "value": "28" + }, + { + "label": "MultipleId", + "value": "387" + }, + { + "label": "marry", + "value": "3386" + }, + { + "label": "Mystery", + "value": "936" + }, + { + "label": "MartialSpi", + "value": "394" + }, + { + "label": "Mother-in-", + "value": "3169" + }, + { + "label": "MoneyGrubb", + "value": "82" + }, + { + "label": "MysterySol", + "value": "185" + }, + { + "label": "MatureProt", + "value": "97" + }, + { + "label": "MythicalBe", + "value": "427" + }, + { + "label": "Movies", + "value": "292" + }, + { + "label": "mindreadin", + "value": "2873" + }, + { + "label": "MagicForma", + "value": "72" + }, + { + "label": "Masterflow", + "value": "3307" + }, + { + "label": "makefine", + "value": "3179" + }, + { + "label": "MagicalTec", + "value": "608" + }, + { + "label": "Munchkin", + "value": "5446" + }, + { + "label": "Maid", + "value": "1560" + }, + { + "label": "Medieval", + "value": "429" + }, + { + "label": "Music", + "value": "186" + }, + { + "label": "MiddleAges", + "value": "5098" + }, + { + "label": "Mysterious", + "value": "3634" + }, + { + "label": "Mozun", + "value": "3351" + }, + { + "label": "MutatedCre", + "value": "320" + }, + { + "label": "malematch", + "value": "3628" + }, + { + "label": "MarvelUniv", + "value": "553" + }, + { + "label": "Mecha", + "value": "804" + }, + { + "label": "MultiplePr", + "value": "501" + }, + { + "label": "magicalcre", + "value": "3444" + }, + { + "label": "Management", + "value": "642" + }, + { + "label": "MaletoFema", + "value": "480" + }, + { + "label": "ManlyGayCo", + "value": "96" + }, + { + "label": "Mercenarie", + "value": "167" + }, + { + "label": "Mutations", + "value": "418" + }, + { + "label": "Manvs.Wild", + "value": "3194" + }, + { + "label": "Masashi", + "value": "3627" + }, + { + "label": "Maids", + "value": "649" + }, + { + "label": "Murders", + "value": "366" + }, + { + "label": "Monk", + "value": "3633" + }, + { + "label": "militaryfa", + "value": "3402" + }, + { + "label": "Militaryin", + "value": "3401" + }, + { + "label": "modernlove", + "value": "1780" + }, + { + "label": "MaleLead", + "value": "716" + }, + { + "label": "MobileGame", + "value": "3717" + }, + { + "label": "MyHeroAcad", + "value": "1340" + }, + { + "label": "ModernDays", + "value": "754" + }, + { + "label": "MultipleTi", + "value": "615" + }, + { + "label": "Merchants", + "value": "527" + }, + { + "label": "MindContro", + "value": "389" + }, + { + "label": "modernmagi", + "value": "5118" + }, + { + "label": "Monster", + "value": "1136" + }, + { + "label": "Mercenary", + "value": "797" + }, + { + "label": "Medicine", + "value": "795" + }, + { + "label": "MobProtago", + "value": "443" + }, + { + "label": "MassiveHar", + "value": "891" + }, + { + "label": "Matriarchy", + "value": "686" + }, + { + "label": "Mythical", + "value": "666" + }, + { + "label": "Models", + "value": "657" + }, + { + "label": "MonsterGir", + "value": "569" + }, + { + "label": "Murder", + "value": "345" + }, + { + "label": "Miracledoc", + "value": "6195" + }, + { + "label": "Myth", + "value": "5079" + }, + { + "label": "Mr.Quin", + "value": "3800" + }, + { + "label": "magician", + "value": "3700" + }, + { + "label": "MaleProtag", + "value": "2900" + }, + { + "label": "MrMo", + "value": "1800" + }, + { + "label": "Malaysian", + "value": "1685" + }, + { + "label": "MartialArt", + "value": "1471" + }, + { + "label": "MultipleWo", + "value": "1092" + }, + { + "label": "Mutation", + "value": "844" + }, + { + "label": "MuteCharac", + "value": "736" + }, + { + "label": "Masturbati", + "value": "733" + }, + { + "label": "metropolis", + "value": "5121" + }, + { + "label": "moonandsta", + "value": "4581" + }, + { + "label": "Metauniver", + "value": "3639" + }, + { + "label": "mermaid", + "value": "2876" + }, + { + "label": "magicalgir", + "value": "1744" + }, + { + "label": "MaleMc", + "value": "1091" + }, + { + "label": "Multiverse", + "value": "1026" + }, + { + "label": "Mystical", + "value": "764" + }, + { + "label": "Mysterious", + "value": "656" + }, + { + "label": "maliciousw", + "value": "5915" + }, + { + "label": "MasterGree", + "value": "5213" + }, + { + "label": "Misaki", + "value": "4865" + }, + { + "label": "MasterPeng", + "value": "4835" + }, + { + "label": "moreastron", + "value": "3804" + }, + { + "label": "mysteries", + "value": "3732" + }, + { + "label": "Miluo", + "value": "2267" + }, + { + "label": "Meowingbig", + "value": "2087" + }, + { + "label": "moonbug", + "value": "1931" + }, + { + "label": "MalaysianN", + "value": "1684" + }, + { + "label": "multiplere", + "value": "1657" + }, + { + "label": "Mythos", + "value": "1651" + }, + { + "label": "Mob", + "value": "1584" + }, + { + "label": "movies", + "value": "1413" + }, + { + "label": "Male-Prota", + "value": "1378" + }, + { + "label": "MartialSpi", + "value": "1364" + }, + { + "label": "Male-Lead", + "value": "1350" + }, + { + "label": "MonsterSoc", + "value": "1151" + }, + { + "label": "Msturbatio", + "value": "1022" + }, + { + "label": "MagicAcade", + "value": "988" + }, + { + "label": "MaleProtag", + "value": "965" + }, + { + "label": "ModernRoma", + "value": "959" + }, + { + "label": "MultipleCP", + "value": "756" + }, + { + "label": "MindBreak", + "value": "621" + }, + { + "label": "Mysterious", + "value": "363" + }, + { + "label": "Martialart", + "value": "6181" + }, + { + "label": "MasterTian", + "value": "6124" + }, + { + "label": "Miha", + "value": "6114" + }, + { + "label": "Mr.Despair", + "value": "6112" + }, + { + "label": "MikaMio", + "value": "6098" + }, + { + "label": "meowhokage", + "value": "6097" + }, + { + "label": "Mushroomma", + "value": "6091" + }, + { + "label": "MadBoyTang", + "value": "6088" + }, + { + "label": "Moonstay", + "value": "6030" + }, + { + "label": "MasterZhiq", + "value": "6018" + }, + { + "label": "meatsauce", + "value": "6015" + }, + { + "label": "MoFenghou", + "value": "6014" + }, + { + "label": "Morningclo", + "value": "6001" + }, + { + "label": "MaifengXia", + "value": "5962" + }, + { + "label": "Milkteapot", + "value": "5932" + }, + { + "label": "Mr.Zhaowho", + "value": "5921" + }, + { + "label": "MarvelWang", + "value": "5875" + }, + { + "label": "Mr.ShuiYun", + "value": "5844" + }, + { + "label": "MuNanzhi", + "value": "5842" + }, + { + "label": "maple", + "value": "5818" + }, + { + "label": "MagicDrago", + "value": "5817" + }, + { + "label": "manhasbeco", + "value": "5759" + }, + { + "label": "monsteridl", + "value": "5727" + }, + { + "label": "Martialart", + "value": "5710" + }, + { + "label": "Millennium", + "value": "5667" + }, + { + "label": "magicpet", + "value": "5626" + }, + { + "label": "Melancholy", + "value": "5620" + }, + { + "label": "moonlightw", + "value": "5598" + }, + { + "label": "MoeHan", + "value": "5597" + }, + { + "label": "MaodongChe", + "value": "5542" + }, + { + "label": "木其", + "value": "5522" + }, + { + "label": "mygrandma", + "value": "5512" + }, + { + "label": "moonnight", + "value": "5508" + }, + { + "label": "MengYuxin", + "value": "5506" + }, + { + "label": "medicinest", + "value": "5484" + }, + { + "label": "maninorang", + "value": "5410" + }, + { + "label": "moonshower", + "value": "5408" + }, + { + "label": "MrBearBear", + "value": "5391" + }, + { + "label": "MasterofSa", + "value": "5261" + }, + { + "label": "MidoriSaku", + "value": "5254" + }, + { + "label": "Mengxinsma", + "value": "5248" + }, + { + "label": "monthand", + "value": "5240" + }, + { + "label": "Maonanbei!", + "value": "5233" + }, + { + "label": "milliondig", + "value": "5228" + }, + { + "label": "marchisado", + "value": "5227" + }, + { + "label": "MaoLinwhol", + "value": "5194" + }, + { + "label": "MatthewisG", + "value": "5188" + }, + { + "label": "MuMushuhua", + "value": "5182" + }, + { + "label": "Moisturizi", + "value": "5176" + }, + { + "label": "Mr.flag", + "value": "5171" + }, + { + "label": "mountainsa", + "value": "5136" + }, + { + "label": "morninglig", + "value": "5135" + }, + { + "label": "middleaged", + "value": "5128" + }, + { + "label": "mysterious", + "value": "5127" + }, + { + "label": "Milf", + "value": "5089" + }, + { + "label": "Mag", + "value": "5084" + }, + { + "label": "Marysue", + "value": "5077" + }, + { + "label": "medicalfem", + "value": "5050" + }, + { + "label": "midnightbl", + "value": "5030" + }, + { + "label": "Mista", + "value": "5025" + }, + { + "label": "moonshadow", + "value": "4980" + }, + { + "label": "Maggie", + "value": "4955" + }, + { + "label": "maninknigh", + "value": "4954" + }, + { + "label": "manwithkid", + "value": "4947" + }, + { + "label": "morningred", + "value": "4946" + }, + { + "label": "magicwhip", + "value": "4930" + }, + { + "label": "MysticBlue", + "value": "4929" + }, + { + "label": "MuZixi", + "value": "4924" + }, + { + "label": "Mysterious", + "value": "4890" + }, + { + "label": "midnightsu", + "value": "4848" + }, + { + "label": "MrStardust", + "value": "4838" + }, + { + "label": "MizukiSpir", + "value": "4823" + }, + { + "label": "MingXi", + "value": "4822" + }, + { + "label": "Makeupforf", + "value": "4812" + }, + { + "label": "Mountainan", + "value": "4780" + }, + { + "label": "Mysterious", + "value": "4769" + }, + { + "label": "mooncrow", + "value": "4763" + }, + { + "label": "midnightli", + "value": "4740" + }, + { + "label": "moltingsna", + "value": "4736" + }, + { + "label": "Marlene&am", + "value": "4731" + }, + { + "label": "Miska10010", + "value": "4694" + }, + { + "label": "MrEarl", + "value": "4671" + }, + { + "label": "meowhum", + "value": "4670" + }, + { + "label": "MoshangHua", + "value": "4655" + }, + { + "label": "meowmeowsl", + "value": "4647" + }, + { + "label": "Manchengma", + "value": "4620" + }, + { + "label": "moonshadow", + "value": "4619" + }, + { + "label": "makepigeon", + "value": "4612" + }, + { + "label": "MaskedPich", + "value": "4538" + }, + { + "label": "MUGEN", + "value": "4528" + }, + { + "label": "MengmeiBin", + "value": "4463" + }, + { + "label": "MoQingyi", + "value": "4449" + }, + { + "label": "man-eating", + "value": "4442" + }, + { + "label": "makemehapp", + "value": "4431" + }, + { + "label": "Myswordisc", + "value": "4393" + }, + { + "label": "madman", + "value": "4390" + }, + { + "label": "midnight", + "value": "4377" + }, + { + "label": "MuChenchen", + "value": "4369" + }, + { + "label": "machine", + "value": "4351" + }, + { + "label": "moonsetsea", + "value": "4321" + }, + { + "label": "maplebirch", + "value": "4315" + }, + { + "label": "midnightma", + "value": "4280" + }, + { + "label": "mrcoud", + "value": "4260" + }, + { + "label": "Meowalook", + "value": "4259" + }, + { + "label": "Mr.Xiaisfr", + "value": "4240" + }, + { + "label": "Mechanical", + "value": "4214" + }, + { + "label": "mungbeanga", + "value": "4159" + }, + { + "label": "madmanandf", + "value": "4114" + }, + { + "label": "Masterofea", + "value": "4103" + }, + { + "label": "MuxiMuxi", + "value": "4078" + }, + { + "label": "MountainSe", + "value": "4077" + }, + { + "label": "meetingdee", + "value": "4060" + }, + { + "label": "Medical", + "value": "4013" + }, + { + "label": "MengXiaohu", + "value": "4010" + }, + { + "label": "Meteorstre", + "value": "4009" + }, + { + "label": "MengLiangq", + "value": "3917" + }, + { + "label": "mustfire", + "value": "3889" + }, + { + "label": "Momiji", + "value": "3849" + }, + { + "label": "Manhua", + "value": "3844" + }, + { + "label": "maplefores", + "value": "3799" + }, + { + "label": "moontea", + "value": "3793" + }, + { + "label": "mywifeisab", + "value": "3784" + }, + { + "label": "mymeatisde", + "value": "3782" + }, + { + "label": "MonkeyKing", + "value": "3759" + }, + { + "label": "MechGod", + "value": "3738" + }, + { + "label": "MoXianyu", + "value": "3660" + }, + { + "label": "Millennium", + "value": "3581" + }, + { + "label": "Mass-produ", + "value": "3516" + }, + { + "label": "Masterishe", + "value": "3512" + }, + { + "label": "MynameisZh", + "value": "3511" + }, + { + "label": "MyGreatQin", + "value": "3487" + }, + { + "label": "Marioeatsc", + "value": "3478" + }, + { + "label": "MakemoneyI", + "value": "3082" + }, + { + "label": "MingDynast", + "value": "3076" + }, + { + "label": "metaphysic", + "value": "3065" + }, + { + "label": "martialart", + "value": "3055" + }, + { + "label": "MakemoneyR", + "value": "3053" + }, + { + "label": "Makemoneys", + "value": "3018" + }, + { + "label": "Motherland", + "value": "3010" + }, + { + "label": "MengBaowea", + "value": "2999" + }, + { + "label": "MakemoneyM", + "value": "2998" + }, + { + "label": "Mother-in-", + "value": "2990" + }, + { + "label": "MakemoneyR", + "value": "2977" + }, + { + "label": "MingDynast", + "value": "2962" + }, + { + "label": "manyfemale", + "value": "2957" + }, + { + "label": "MakemoneyR", + "value": "2950" + }, + { + "label": "MakemoneyC", + "value": "2945" + }, + { + "label": "MakemoneyM", + "value": "2937" + }, + { + "label": "MingDynast", + "value": "2934" + }, + { + "label": "MoeShinkaw", + "value": "2930" + }, + { + "label": "ModernLife", + "value": "2908" + }, + { + "label": "Merchant", + "value": "2886" + }, + { + "label": "Malemainch", + "value": "2878" + }, + { + "label": "MaskedArmo", + "value": "2860" + }, + { + "label": "mapleleafb", + "value": "2854" + }, + { + "label": "Milkgather", + "value": "2831" + }, + { + "label": "萌图", + "value": "2821" + }, + { + "label": "MasterofSi", + "value": "2806" + }, + { + "label": "Moedye", + "value": "2790" + }, + { + "label": "mywifeisya", + "value": "2754" + }, + { + "label": "mambafight", + "value": "2718" + }, + { + "label": "medicineme", + "value": "2688" + }, + { + "label": "MoonlightS", + "value": "2661" + }, + { + "label": "Makeafortu", + "value": "2638" + }, + { + "label": "MyLubanThi", + "value": "2630" + }, + { + "label": "Mofamily", + "value": "2625" + }, + { + "label": "mixedintwo", + "value": "2601" + }, + { + "label": "MagicOne", + "value": "2595" + }, + { + "label": "mythicalfi", + "value": "2575" + }, + { + "label": "Moyangison", + "value": "2570" + }, + { + "label": "Mr.Huo", + "value": "2542" + }, + { + "label": "MarvelPudd", + "value": "2540" + }, + { + "label": "maninnarut", + "value": "2486" + }, + { + "label": "Moonlikeah", + "value": "2467" + }, + { + "label": "mywife", + "value": "2448" + }, + { + "label": "mythicalma", + "value": "2440" + }, + { + "label": "MangoKK", + "value": "2435" + }, + { + "label": "mythunpara", + "value": "2408" + }, + { + "label": "MojiaAeros", + "value": "2396" + }, + { + "label": "MagicTides", + "value": "2393" + }, + { + "label": "millionord", + "value": "2370" + }, + { + "label": "MojiaHills", + "value": "2355" + }, + { + "label": "milkgrandm", + "value": "2278" + }, + { + "label": "Masterball", + "value": "2277" + }, + { + "label": "mynameista", + "value": "2270" + }, + { + "label": "Masquerade", + "value": "2253" + }, + { + "label": "makeamirac", + "value": "2248" + }, + { + "label": "moreandmor", + "value": "2226" + }, + { + "label": "mylittlesi", + "value": "2225" + }, + { + "label": "mudbodhisa", + "value": "2215" + }, + { + "label": "mustdo", + "value": "2199" + }, + { + "label": "man", + "value": "2194" + }, + { + "label": "MistyFlyin", + "value": "2189" + }, + { + "label": "Mistresspl", + "value": "2157" + }, + { + "label": "Mountainsa", + "value": "2154" + }, + { + "label": "meetthebea", + "value": "2151" + }, + { + "label": "MarvelKing", + "value": "2135" + }, + { + "label": "movingbean", + "value": "2127" + }, + { + "label": "meowmeow", + "value": "2080" + }, + { + "label": "MynameisDa", + "value": "2079" + }, + { + "label": "慕阳", + "value": "2057" + }, + { + "label": "MoXueqing", + "value": "2030" + }, + { + "label": "MingjiaoTi", + "value": "2026" + }, + { + "label": "Mr.EasyPro", + "value": "1987" + }, + { + "label": "MarquisofB", + "value": "1953" + }, + { + "label": "Mingjiao", + "value": "1945" + }, + { + "label": "Moonsea", + "value": "1923" + }, + { + "label": "musicwilll", + "value": "1915" + }, + { + "label": "MaskedAce", + "value": "1864" + }, + { + "label": "Mountainsa", + "value": "1857" + }, + { + "label": "MentalIlln", + "value": "1815" + }, + { + "label": "Moonwing", + "value": "1789" + }, + { + "label": "math", + "value": "1781" + }, + { + "label": "marvelworl", + "value": "1779" + }, + { + "label": "MarriedCou", + "value": "1751" + }, + { + "label": "MemoryReve", + "value": "1740" + }, + { + "label": "Morallessp", + "value": "1738" + }, + { + "label": "Matchmadei", + "value": "1736" + }, + { + "label": "Multipleid", + "value": "1729" + }, + { + "label": "Mimicry", + "value": "1711" + }, + { + "label": "MonsterTar", + "value": "1689" + }, + { + "label": "MultipleLe", + "value": "1650" + }, + { + "label": "Massacre", + "value": "1634" + }, + { + "label": "Mukbang", + "value": "1633" + }, + { + "label": "MultipleWo", + "value": "1617" + }, + { + "label": "MsPerfect", + "value": "1611" + }, + { + "label": "MrSly", + "value": "1610" + }, + { + "label": "Ministryof", + "value": "1609" + }, + { + "label": "MagicalBat", + "value": "1587" + }, + { + "label": "Mutan", + "value": "1574" + }, + { + "label": "Misunderst", + "value": "1565" + }, + { + "label": "MarriageCo", + "value": "1543" + }, + { + "label": "MemoryLoss", + "value": "1519" + }, + { + "label": "MCStrongFr", + "value": "1509" + }, + { + "label": "MorallyAmb", + "value": "1494" + }, + { + "label": "MindReader", + "value": "1484" + }, + { + "label": "Manipulati", + "value": "1480" + }, + { + "label": "MultipleVe", + "value": "1470" + }, + { + "label": "Millionair", + "value": "1459" + }, + { + "label": "MagicalAbi", + "value": "1453" + }, + { + "label": "Middleage", + "value": "1447" + }, + { + "label": "mandaloria", + "value": "1434" + }, + { + "label": "MultipleBo", + "value": "1393" + }, + { + "label": "ModerDays", + "value": "1370" + }, + { + "label": "MutantPowe", + "value": "1345" + }, + { + "label": "Mismatched", + "value": "1343" + }, + { + "label": "multipleid", + "value": "1295" + }, + { + "label": "Maleprotag", + "value": "1287" + }, + { + "label": "MultiplePo", + "value": "1277" + }, + { + "label": "MaleProtag", + "value": "1261" + }, + { + "label": "Married", + "value": "1235" + }, + { + "label": "Master-App", + "value": "1228" + }, + { + "label": "MaleMain-l", + "value": "1208" + }, + { + "label": "MoneyGrumb", + "value": "1199" + }, + { + "label": "Monogamy", + "value": "1197" + }, + { + "label": "MultipleHi", + "value": "1188" + }, + { + "label": "MultipleMo", + "value": "1167" + }, + { + "label": "Mutualcrus", + "value": "1161" + }, + { + "label": "MultipleTr", + "value": "1152" + }, + { + "label": "MagicWorld", + "value": "1114" + }, + { + "label": "MedicalKno", + "value": "1110" + }, + { + "label": "Mysterious", + "value": "1096" + }, + { + "label": "MaleProtag", + "value": "1079" + }, + { + "label": "Massive", + "value": "1073" + }, + { + "label": "Misunderst", + "value": "1072" + }, + { + "label": "ModernKnow", + "value": "825" + }, + { + "label": "Mangaka", + "value": "809" + }, + { + "label": "Manipulati", + "value": "597" + }, + { + "label": "Marriageof", + "value": "489" + }, + { + "label": "MultipleRe", + "value": "435" + }, + { + "label": "Master-Ser", + "value": "361" + }, + { + "label": "Master-Dis", + "value": "304" + }, + { + "label": "MultipleTr", + "value": "293" + }, + { + "label": "Masochisti", + "value": "45" + }, + { + "label": "MultiplePe", + "value": "20" + }, + { + "label": "Naruto", + "value": "335" + }, + { + "label": "NoCp", + "value": "1605" + }, + { + "label": "女神", + "value": "3212" + }, + { + "label": "Nogoldenfi", + "value": "3145" + }, + { + "label": "Nodiscipli", + "value": "3249" + }, + { + "label": "Nationalis", + "value": "305" + }, + { + "label": "Nobles", + "value": "280" + }, + { + "label": "Nakaji", + "value": "3614" + }, + { + "label": "NaiveProta", + "value": "75" + }, + { + "label": "NBA", + "value": "617" + }, + { + "label": "NoRomance", + "value": "1015" + }, + { + "label": "noheroine", + "value": "3262" + }, + { + "label": "Non-humanP", + "value": "851" + }, + { + "label": "Necromance", + "value": "422" + }, + { + "label": "Ninjas", + "value": "460" + }, + { + "label": "NA", + "value": "791" + }, + { + "label": "Non-System", + "value": "1311" + }, + { + "label": "NotHarem", + "value": "996" + }, + { + "label": "NPC", + "value": "3141" + }, + { + "label": "Netori", + "value": "78" + }, + { + "label": "NationalDi", + "value": "3590" + }, + { + "label": "Need", + "value": "3751" + }, + { + "label": "Netred", + "value": "3712" + }, + { + "label": "NoCheats", + "value": "4354" + }, + { + "label": "Near-Death", + "value": "76" + }, + { + "label": "No-Harem", + "value": "5082" + }, + { + "label": "Netorare", + "value": "77" + }, + { + "label": "NonHuman", + "value": "1111" + }, + { + "label": "nobledaugh", + "value": "6190" + }, + { + "label": "NoHarem", + "value": "1290" + }, + { + "label": "Non-legacy", + "value": "5448" + }, + { + "label": "networkdis", + "value": "4644" + }, + { + "label": "Necromancy", + "value": "4444" + }, + { + "label": "nightsilen", + "value": "1891" + }, + { + "label": "Nurses", + "value": "637" + }, + { + "label": "Nightmares", + "value": "461" + }, + { + "label": "No.XNUMXon", + "value": "3622" + }, + { + "label": "Nine-Taile", + "value": "2178" + }, + { + "label": "NoSystem", + "value": "1537" + }, + { + "label": "Navy", + "value": "1097" + }, + { + "label": "Nudity", + "value": "972" + }, + { + "label": "女强", + "value": "6175" + }, + { + "label": "nowife", + "value": "5744" + }, + { + "label": "nohouse", + "value": "5743" + }, + { + "label": "Nocar", + "value": "5742" + }, + { + "label": "NanaSauceL", + "value": "5621" + }, + { + "label": "nicheoccup", + "value": "5125" + }, + { + "label": "noregrets", + "value": "4855" + }, + { + "label": "nomooninma", + "value": "4733" + }, + { + "label": "nightrain", + "value": "4651" + }, + { + "label": "notbad", + "value": "4526" + }, + { + "label": "NotIke", + "value": "4524" + }, + { + "label": "nodream", + "value": "3906" + }, + { + "label": "Noble", + "value": "1600" + }, + { + "label": "NonHumanPr", + "value": "1078" + }, + { + "label": "Neet", + "value": "645" + }, + { + "label": "Nightmare", + "value": "346" + }, + { + "label": "Novel", + "value": "6184" + }, + { + "label": "nextyear", + "value": "6141" + }, + { + "label": "Nagasakiha", + "value": "6099" + }, + { + "label": "newbie", + "value": "6071" + }, + { + "label": "ninthsweet", + "value": "6066" + }, + { + "label": "NikaBaka", + "value": "6013" + }, + { + "label": "NightAllur", + "value": "6000" + }, + { + "label": "NightSheng", + "value": "5999" + }, + { + "label": "NightDance", + "value": "5998" + }, + { + "label": "Narutosix", + "value": "5984" + }, + { + "label": "namelessna", + "value": "5971" + }, + { + "label": "Neveruseso", + "value": "5964" + }, + { + "label": "nocarton", + "value": "5961" + }, + { + "label": "NineShaoSh", + "value": "5821" + }, + { + "label": "nonpondfis", + "value": "5799" + }, + { + "label": "No.XNUMXXi", + "value": "5762" + }, + { + "label": "NarutoNovi", + "value": "5640" + }, + { + "label": "Northeastb", + "value": "5592" + }, + { + "label": "nightlangu", + "value": "5571" + }, + { + "label": "NegaNebulu", + "value": "5502" + }, + { + "label": "NaiNaiHeTi", + "value": "5500" + }, + { + "label": "noisyfish", + "value": "5495" + }, + { + "label": "NorthSeaMo", + "value": "5487" + }, + { + "label": "notnew", + "value": "5468" + }, + { + "label": "newnine", + "value": "5462" + }, + { + "label": "NorthDepar", + "value": "5426" + }, + { + "label": "noon", + "value": "5374" + }, + { + "label": "NaiTsujiCi", + "value": "5351" + }, + { + "label": "NoCPRelaxe", + "value": "5317" + }, + { + "label": "NoCPreveng", + "value": "5316" + }, + { + "label": "NineHeaven", + "value": "5253" + }, + { + "label": "Numberofwo", + "value": "5133" + }, + { + "label": "NSFW", + "value": "5087" + }, + { + "label": "NiShiliu", + "value": "5043" + }, + { + "label": "notcold", + "value": "4986" + }, + { + "label": "NagatoYuki", + "value": "4963" + }, + { + "label": "Neepeatsme", + "value": "4942" + }, + { + "label": "Nidoriya", + "value": "4934" + }, + { + "label": "Nasida", + "value": "4907" + }, + { + "label": "Noblenessi", + "value": "4832" + }, + { + "label": "nostring", + "value": "4825" + }, + { + "label": "NamoAmitab", + "value": "4805" + }, + { + "label": "nolove", + "value": "4768" + }, + { + "label": "nightwhite", + "value": "4758" + }, + { + "label": "Northstar", + "value": "4719" + }, + { + "label": "NagiNagibe", + "value": "4559" + }, + { + "label": "neverdream", + "value": "4539" + }, + { + "label": "NineStarMa", + "value": "4523" + }, + { + "label": "Nari", + "value": "4481" + }, + { + "label": "nottowersa", + "value": "4478" + }, + { + "label": "newspapera", + "value": "4471" + }, + { + "label": "nevertooba", + "value": "4415" + }, + { + "label": "noshadow", + "value": "4408" + }, + { + "label": "Needsquirr", + "value": "4376" + }, + { + "label": "Non-HumanM", + "value": "4359" + }, + { + "label": "NTL", + "value": "4358" + }, + { + "label": "NoblesPoli", + "value": "4357" + }, + { + "label": "narutovill", + "value": "4306" + }, + { + "label": "Nine", + "value": "4298" + }, + { + "label": "nightsleep", + "value": "4237" + }, + { + "label": "notgreedy", + "value": "4224" + }, + { + "label": "nb9527", + "value": "4129" + }, + { + "label": "NetherNine", + "value": "4127" + }, + { + "label": "normalluck", + "value": "4079" + }, + { + "label": "nineshipju", + "value": "4051" + }, + { + "label": "nineonemor", + "value": "4038" + }, + { + "label": "notfalling", + "value": "4029" + }, + { + "label": "你的答案", + "value": "4028" + }, + { + "label": "ninthinthe", + "value": "3977" + }, + { + "label": "NingYi", + "value": "3956" + }, + { + "label": "neromywife", + "value": "3940" + }, + { + "label": "Ninedays", + "value": "3931" + }, + { + "label": "Nunknightw", + "value": "3908" + }, + { + "label": "NuclearBea", + "value": "3830" + }, + { + "label": "no", + "value": "3808" + }, + { + "label": "NorthMu", + "value": "3768" + }, + { + "label": "Notraceofm", + "value": "3762" + }, + { + "label": "Nativebamb", + "value": "3727" + }, + { + "label": "notguilty", + "value": "3725" + }, + { + "label": "Northeastb", + "value": "3659" + }, + { + "label": "natureover", + "value": "3649" + }, + { + "label": "nationalmy", + "value": "3641" + }, + { + "label": "ninepositi", + "value": "3618" + }, + { + "label": "narutodrag", + "value": "3617" + }, + { + "label": "Nim", + "value": "3579" + }, + { + "label": "NuShengsoo", + "value": "3510" + }, + { + "label": "notwo", + "value": "3466" + }, + { + "label": "nightdance", + "value": "2725" + }, + { + "label": "Noless", + "value": "2696" + }, + { + "label": "Nowadays", + "value": "2604" + }, + { + "label": "NarutoxRea", + "value": "2588" + }, + { + "label": "Nidouzi", + "value": "2587" + }, + { + "label": "noncat", + "value": "2578" + }, + { + "label": "neverfail", + "value": "2567" + }, + { + "label": "notlevelth", + "value": "2564" + }, + { + "label": "notscary", + "value": "2488" + }, + { + "label": "NarutoQuiz", + "value": "2485" + }, + { + "label": "narutoceda", + "value": "2484" + }, + { + "label": "NarutoClou", + "value": "2479" + }, + { + "label": "Nine-color", + "value": "2436" + }, + { + "label": "NiangkouSa", + "value": "2337" + }, + { + "label": "Nanshen", + "value": "2307" + }, + { + "label": "NiuBao", + "value": "2085" + }, + { + "label": "NineWarsof", + "value": "2082" + }, + { + "label": "Nosnacks", + "value": "2081" + }, + { + "label": "NinefoldSe", + "value": "1959" + }, + { + "label": "Ninjapirat", + "value": "1868" + }, + { + "label": "nightfire", + "value": "1863" + }, + { + "label": "饕牛", + "value": "1831" + }, + { + "label": "ninja", + "value": "1756" + }, + { + "label": "Non-Humanl", + "value": "1667" + }, + { + "label": "Napoleon", + "value": "1497" + }, + { + "label": "nihilism", + "value": "1437" + }, + { + "label": "nonhuman", + "value": "1401" + }, + { + "label": "NoPairing", + "value": "1278" + }, + { + "label": "NationBuil", + "value": "1115" + }, + { + "label": "NotYaoi", + "value": "1032" + }, + { + "label": "nokillingm", + "value": "1017" + }, + { + "label": "Narcissist", + "value": "469" + }, + { + "label": "Non-humano", + "value": "143" + }, + { + "label": "Overhead", + "value": "3225" + }, + { + "label": "OnePiece", + "value": "336" + }, + { + "label": "Orientalfa", + "value": "694" + }, + { + "label": "overbearin", + "value": "3443" + }, + { + "label": "Obsession", + "value": "3831" + }, + { + "label": "openingflo", + "value": "3149" + }, + { + "label": "Onlinegame", + "value": "3168" + }, + { + "label": "otherworld", + "value": "5685" + }, + { + "label": "OuterSpace", + "value": "409" + }, + { + "label": "openflow", + "value": "3361" + }, + { + "label": "Otaku", + "value": "472" + }, + { + "label": "OPMC", + "value": "892" + }, + { + "label": "Omegaverse", + "value": "301" + }, + { + "label": "OlderLoveI", + "value": "364" + }, + { + "label": "onlinegame", + "value": "3394" + }, + { + "label": "Overpowere", + "value": "876" + }, + { + "label": "Open", + "value": "3259" + }, + { + "label": "Orphans", + "value": "428" + }, + { + "label": "originalco", + "value": "3238" + }, + { + "label": "Onocat", + "value": "3406" + }, + { + "label": "Orcs", + "value": "720" + }, + { + "label": "Openupwast", + "value": "3264" + }, + { + "label": "ObsessiveL", + "value": "205" + }, + { + "label": "OrganizedC", + "value": "174" + }, + { + "label": "Orientalde", + "value": "5705" + }, + { + "label": "Office", + "value": "3606" + }, + { + "label": "OfficeRoma", + "value": "517" + }, + { + "label": "OnlineRoma", + "value": "638" + }, + { + "label": "OPProtagon", + "value": "1183" + }, + { + "label": "Owner", + "value": "3636" + }, + { + "label": "OnlineGame", + "value": "762" + }, + { + "label": "overpower", + "value": "6156" + }, + { + "label": "onlyloveyo", + "value": "1952" + }, + { + "label": "Orphan", + "value": "1247" + }, + { + "label": "Organizati", + "value": "1056" + }, + { + "label": "Overpowere", + "value": "864" + }, + { + "label": "OldTeaTree", + "value": "4997" + }, + { + "label": "orientalfa", + "value": "4273" + }, + { + "label": "ownerofwhi", + "value": "4025" + }, + { + "label": "OtomeGame", + "value": "1580" + }, + { + "label": "overpowere", + "value": "1394" + }, + { + "label": "OnePunchMa", + "value": "1328" + }, + { + "label": "Orc", + "value": "1298" + }, + { + "label": "OpFemalePr", + "value": "1189" + }, + { + "label": "Overlord", + "value": "1184" + }, + { + "label": "Onlinegame", + "value": "6193" + }, + { + "label": "Orientalle", + "value": "6035" + }, + { + "label": "Observer23", + "value": "5960" + }, + { + "label": "oneminustw", + "value": "5898" + }, + { + "label": "Ordinary", + "value": "5887" + }, + { + "label": "OnenightHo", + "value": "5880" + }, + { + "label": "origamista", + "value": "5863" + }, + { + "label": "Ottointhek", + "value": "5802" + }, + { + "label": "Onebyone", + "value": "5661" + }, + { + "label": "OldWuupsta", + "value": "5650" + }, + { + "label": "OneSwordLi", + "value": "5560" + }, + { + "label": "OrangeOche", + "value": "5510" + }, + { + "label": "OuyangRuox", + "value": "5458" + }, + { + "label": "OldYdoesno", + "value": "5434" + }, + { + "label": "oldfacesli", + "value": "5379" + }, + { + "label": "OverheadSu", + "value": "5321" + }, + { + "label": "oldwalnut", + "value": "5297" + }, + { + "label": "oldfashion", + "value": "5288" + }, + { + "label": "owe", + "value": "5252" + }, + { + "label": "Outstandin", + "value": "5179" + }, + { + "label": "OldAlfred", + "value": "5099" + }, + { + "label": "Outdoors", + "value": "5092" + }, + { + "label": "oldclawmac", + "value": "5016" + }, + { + "label": "Oneofthebe", + "value": "5014" + }, + { + "label": "Orientalre", + "value": "4927" + }, + { + "label": "OhLL", + "value": "4842" + }, + { + "label": "OneLeafSea", + "value": "4739" + }, + { + "label": "observerwx", + "value": "4735" + }, + { + "label": "Oldies", + "value": "4606" + }, + { + "label": "Otakuisnot", + "value": "4585" + }, + { + "label": "ourraccoon", + "value": "4561" + }, + { + "label": "oldlady", + "value": "4553" + }, + { + "label": "oldmonth", + "value": "4480" + }, + { + "label": "outingbook", + "value": "4429" + }, + { + "label": "OneSwordEm", + "value": "4409" + }, + { + "label": "omega", + "value": "4341" + }, + { + "label": "Over-Power", + "value": "4325" + }, + { + "label": "Olderlovei", + "value": "4324" + }, + { + "label": "onemouthfu", + "value": "4047" + }, + { + "label": "oldchicken", + "value": "3997" + }, + { + "label": "Otoichi", + "value": "3929" + }, + { + "label": "Operationr", + "value": "3925" + }, + { + "label": "Overseasca", + "value": "3778" + }, + { + "label": "oldmaneati", + "value": "3770" + }, + { + "label": "orangeglor", + "value": "3765" + }, + { + "label": "orientalsa", + "value": "3747" + }, + { + "label": "Originalfi", + "value": "3702" + }, + { + "label": "Outlawluna", + "value": "3684" + }, + { + "label": "One", + "value": "3677" + }, + { + "label": "Orangespar", + "value": "3490" + }, + { + "label": "originalco", + "value": "3046" + }, + { + "label": "openingflo", + "value": "3020" + }, + { + "label": "OverheadHi", + "value": "2905" + }, + { + "label": "offical", + "value": "2898" + }, + { + "label": "Operation", + "value": "2896" + }, + { + "label": "One-Piece", + "value": "2874" + }, + { + "label": "onemeterst", + "value": "2794" + }, + { + "label": "Origuchi", + "value": "2742" + }, + { + "label": "onlyyouth", + "value": "2736" + }, + { + "label": "onemelonri", + "value": "2724" + }, + { + "label": "onetree", + "value": "2723" + }, + { + "label": "Oneflower", + "value": "2722" + }, + { + "label": "Oneyearold", + "value": "2715" + }, + { + "label": "Onepunchto", + "value": "2709" + }, + { + "label": "OriginalYe", + "value": "2705" + }, + { + "label": "oldfaceunc", + "value": "2698" + }, + { + "label": "OneLeafRed", + "value": "2499" + }, + { + "label": "Onepunchmo", + "value": "2495" + }, + { + "label": "oldtombrob", + "value": "2487" + }, + { + "label": "OTTGroupCh", + "value": "2472" + }, + { + "label": "Oldghostsm", + "value": "2469" + }, + { + "label": "ohmygod", + "value": "2446" + }, + { + "label": "oooobe", + "value": "2392" + }, + { + "label": "Openyourey", + "value": "2327" + }, + { + "label": "OriginalUn", + "value": "2325" + }, + { + "label": "onparadise", + "value": "2283" + }, + { + "label": "orangeappl", + "value": "2239" + }, + { + "label": "Obanbrothe", + "value": "2158" + }, + { + "label": "olddemon", + "value": "2129" + }, + { + "label": "Otezetta", + "value": "2117" + }, + { + "label": "Onethousan", + "value": "2083" + }, + { + "label": "oldfisheat", + "value": "2044" + }, + { + "label": "OneSwordFl", + "value": "2039" + }, + { + "label": "OldQinpeop", + "value": "1999" + }, + { + "label": "oldage", + "value": "1910" + }, + { + "label": "openasmall", + "value": "1806" + }, + { + "label": "OriginalON", + "value": "1647" + }, + { + "label": "On-HookSys", + "value": "1615" + }, + { + "label": "OPheroine", + "value": "1469" + }, + { + "label": "onepiece", + "value": "1411" + }, + { + "label": "Overpowere", + "value": "1214" + }, + { + "label": "Online", + "value": "1117" + }, + { + "label": "Onepunch", + "value": "1063" + }, + { + "label": "Outcasts", + "value": "991" + }, + { + "label": "Orcsworld", + "value": "966" + }, + { + "label": "OrphanMC", + "value": "933" + }, + { + "label": "Overpowerd", + "value": "821" + }, + { + "label": "Overprotec", + "value": "717" + }, + { + "label": "Overpowere", + "value": "41" + }, + { + "label": "President", + "value": "1715" + }, + { + "label": "PoortoRich", + "value": "132" + }, + { + "label": "Princess", + "value": "1527" + }, + { + "label": "prince", + "value": "3163" + }, + { + "label": "protectsho", + "value": "3227" + }, + { + "label": "Plane", + "value": "3120" + }, + { + "label": "Paranoid", + "value": "1168" + }, + { + "label": "positiveen", + "value": "3202" + }, + { + "label": "Proud", + "value": "3316" + }, + { + "label": "Possession", + "value": "605" + }, + { + "label": "Pets", + "value": "107" + }, + { + "label": "Politics", + "value": "43" + }, + { + "label": "Player", + "value": "1562" + }, + { + "label": "PastandPre", + "value": "3171" + }, + { + "label": "Polygamy", + "value": "282" + }, + { + "label": "Profession", + "value": "3201" + }, + { + "label": "Post-apoca", + "value": "401" + }, + { + "label": "PowerCoupl", + "value": "47" + }, + { + "label": "Pet", + "value": "1206" + }, + { + "label": "Pokemon", + "value": "587" + }, + { + "label": "Pregnancy", + "value": "83" + }, + { + "label": "PureLove", + "value": "3838" + }, + { + "label": "Possessive", + "value": "4" + }, + { + "label": "Powerhouse", + "value": "3270" + }, + { + "label": "ParallelWo", + "value": "458" + }, + { + "label": "Pros", + "value": "3300" + }, + { + "label": "practice", + "value": "3346" + }, + { + "label": "polarflow", + "value": "3407" + }, + { + "label": "Pirates", + "value": "546" + }, + { + "label": "propertyst", + "value": "3569" + }, + { + "label": "Police", + "value": "419" + }, + { + "label": "Poisonoust", + "value": "3426" + }, + { + "label": "poisondoct", + "value": "3409" + }, + { + "label": "Powerfulmi", + "value": "3104" + }, + { + "label": "PreviousLi", + "value": "138" + }, + { + "label": "PastPlaysa", + "value": "98" + }, + { + "label": "policyflow", + "value": "3198" + }, + { + "label": "PoorProtag", + "value": "522" + }, + { + "label": "Profession", + "value": "6153" + }, + { + "label": "puppy", + "value": "3599" + }, + { + "label": "PastTrauma", + "value": "114" + }, + { + "label": "practicefl", + "value": "3611" + }, + { + "label": "PervertedP", + "value": "281" + }, + { + "label": "PopularLov", + "value": "592" + }, + { + "label": "PillConcoc", + "value": "400" + }, + { + "label": "Policemen", + "value": "3587" + }, + { + "label": "PrinceofTe", + "value": "1306" + }, + { + "label": "Physician", + "value": "3710" + }, + { + "label": "Poisons", + "value": "370" + }, + { + "label": "palacemaid", + "value": "3452" + }, + { + "label": "Parody", + "value": "620" + }, + { + "label": "ProactiveP", + "value": "423" + }, + { + "label": "PowerStrug", + "value": "690" + }, + { + "label": "PragmaticP", + "value": "593" + }, + { + "label": "PrimeMinis", + "value": "3775" + }, + { + "label": "Psychologi", + "value": "1045" + }, + { + "label": "Polyandry", + "value": "669" + }, + { + "label": "PsychicPow", + "value": "715" + }, + { + "label": "PlayfulPro", + "value": "714" + }, + { + "label": "Psychopath", + "value": "498" + }, + { + "label": "PoisonConc", + "value": "3892" + }, + { + "label": "Pharmacist", + "value": "689" + }, + { + "label": "Prison", + "value": "560" + }, + { + "label": "Personalit", + "value": "137" + }, + { + "label": "Popularsci", + "value": "3335" + }, + { + "label": "Pony", + "value": "3603" + }, + { + "label": "PillBasedC", + "value": "399" + }, + { + "label": "PillConcot", + "value": "674" + }, + { + "label": "Playboys", + "value": "653" + }, + { + "label": "PortalFant", + "value": "3421" + }, + { + "label": "Priests", + "value": "707" + }, + { + "label": "Programmer", + "value": "704" + }, + { + "label": "ParentComp", + "value": "650" + }, + { + "label": "PretendLov", + "value": "250" + }, + { + "label": "Progressio", + "value": "1652" + }, + { + "label": "Pilots", + "value": "1592" + }, + { + "label": "ParallelWo", + "value": "819" + }, + { + "label": "Prophecies", + "value": "718" + }, + { + "label": "PreviousLi", + "value": "685" + }, + { + "label": "Prostitute", + "value": "447" + }, + { + "label": "Phoenixes", + "value": "236" + }, + { + "label": "Purple", + "value": "4493" + }, + { + "label": "Protagonis", + "value": "1640" + }, + { + "label": "Powerfulco", + "value": "1355" + }, + { + "label": "PortableSp", + "value": "951" + }, + { + "label": "Poetry", + "value": "731" + }, + { + "label": "Precogniti", + "value": "724" + }, + { + "label": "PoliteProt", + "value": "684" + }, + { + "label": "pigeonbeek", + "value": "5896" + }, + { + "label": "Puppet", + "value": "4301" + }, + { + "label": "Popi", + "value": "4244" + }, + { + "label": "PurpleEmpe", + "value": "4155" + }, + { + "label": "pity", + "value": "3772" + }, + { + "label": "Priest", + "value": "3708" + }, + { + "label": "purpleslim", + "value": "3540" + }, + { + "label": "PrinceQing", + "value": "2631" + }, + { + "label": "PerfectWor", + "value": "1332" + }, + { + "label": "PovertyAll", + "value": "1280" + }, + { + "label": "Possessive", + "value": "1211" + }, + { + "label": "Pirate", + "value": "1166" + }, + { + "label": "Protagonis", + "value": "1037" + }, + { + "label": "Primitivew", + "value": "967" + }, + { + "label": "Patriarch", + "value": "855" + }, + { + "label": "Philosophi", + "value": "774" + }, + { + "label": "Parasites", + "value": "331" + }, + { + "label": "Part-TimeJ", + "value": "268" + }, + { + "label": "Palacefigh", + "value": "6174" + }, + { + "label": "pangolin", + "value": "6105" + }, + { + "label": "pleasehave", + "value": "6084" + }, + { + "label": "Pipisanqin", + "value": "6079" + }, + { + "label": "peachmelon", + "value": "6069" + }, + { + "label": "planeapple", + "value": "6024" + }, + { + "label": "Psychokill", + "value": "5979" + }, + { + "label": "Playaswate", + "value": "5973" + }, + { + "label": "pokemonelf", + "value": "5957" + }, + { + "label": "Putdownthe", + "value": "5924" + }, + { + "label": "Positive", + "value": "5888" + }, + { + "label": "Possessive", + "value": "5886" + }, + { + "label": "Prodigalso", + "value": "5857" + }, + { + "label": "Purple-hai", + "value": "5853" + }, + { + "label": "Peoplepass", + "value": "5824" + }, + { + "label": "PotatoRawS", + "value": "5796" + }, + { + "label": "PurpleDuri", + "value": "5789" + }, + { + "label": "personusin", + "value": "5772" + }, + { + "label": "peoplewhoe", + "value": "5723" + }, + { + "label": "purelovefa", + "value": "5719" + }, + { + "label": "prodigal", + "value": "5714" + }, + { + "label": "PurebloodS", + "value": "5657" + }, + { + "label": "perfecttom", + "value": "5581" + }, + { + "label": "passerbylo", + "value": "5557" + }, + { + "label": "passingmap", + "value": "5527" + }, + { + "label": "PickingupY", + "value": "5526" + }, + { + "label": "puresenior", + "value": "5524" + }, + { + "label": "pencildraw", + "value": "5474" + }, + { + "label": "Parenting", + "value": "5454" + }, + { + "label": "Pochita", + "value": "5428" + }, + { + "label": "program", + "value": "5416" + }, + { + "label": "playYaoer", + "value": "5358" + }, + { + "label": "Peacefulan", + "value": "5269" + }, + { + "label": "penhero", + "value": "5231" + }, + { + "label": "pouringice", + "value": "5218" + }, + { + "label": "porkbuns", + "value": "5204" + }, + { + "label": "purplebrok", + "value": "5168" + }, + { + "label": "papergrayo", + "value": "5164" + }, + { + "label": "Peoplearef", + "value": "5144" + }, + { + "label": "PrettyGirl", + "value": "5073" + }, + { + "label": "Popular", + "value": "5072" + }, + { + "label": "palace", + "value": "5048" + }, + { + "label": "pleasecall", + "value": "5018" + }, + { + "label": "pomeloslee", + "value": "4987" + }, + { + "label": "PoleStarWh", + "value": "4983" + }, + { + "label": "Pepsi", + "value": "4960" + }, + { + "label": "Passer-by", + "value": "4940" + }, + { + "label": "Playingcat", + "value": "4922" + }, + { + "label": "pentaclekn", + "value": "4905" + }, + { + "label": "Preferthew", + "value": "4869" + }, + { + "label": "probabilit", + "value": "4844" + }, + { + "label": "PurpleNaya", + "value": "4808" + }, + { + "label": "PraiseforD", + "value": "4806" + }, + { + "label": "pervertedi", + "value": "4786" + }, + { + "label": "Prosperous", + "value": "4784" + }, + { + "label": "Poriacocos", + "value": "4712" + }, + { + "label": "Piscesscum", + "value": "4695" + }, + { + "label": "Poison1", + "value": "4686" + }, + { + "label": "Pigeonsare", + "value": "4673" + }, + { + "label": "Pennamenot", + "value": "4626" + }, + { + "label": "PeasPigeon", + "value": "4544" + }, + { + "label": "Protectthe", + "value": "4512" + }, + { + "label": "prehistori", + "value": "4482" + }, + { + "label": "Pickingupp", + "value": "4436" + }, + { + "label": "PirateSumm", + "value": "4418" + }, + { + "label": "Pleasesixt", + "value": "4396" + }, + { + "label": "PowerGener", + "value": "4375" + }, + { + "label": "poundedric", + "value": "4365" + }, + { + "label": "Poor", + "value": "4355" + }, + { + "label": "PrimitiveT", + "value": "4336" + }, + { + "label": "PipiGray", + "value": "4272" + }, + { + "label": "Phosphorus", + "value": "4264" + }, + { + "label": "pigc", + "value": "4262" + }, + { + "label": "PokémonUn", + "value": "4221" + }, + { + "label": "purewhiteo", + "value": "4213" + }, + { + "label": "Profession", + "value": "4202" + }, + { + "label": "Prosperity", + "value": "4131" + }, + { + "label": "pigeonsnev", + "value": "4113" + }, + { + "label": "pleasehitm", + "value": "4097" + }, + { + "label": "purepigeon", + "value": "4015" + }, + { + "label": "porridge", + "value": "4008" + }, + { + "label": "Potatoesar", + "value": "3993" + }, + { + "label": "PlaneWars", + "value": "3972" + }, + { + "label": "Persistenc", + "value": "3872" + }, + { + "label": "Panda&0", + "value": "3865" + }, + { + "label": "Programmin", + "value": "3848" + }, + { + "label": "palebluefl", + "value": "3817" + }, + { + "label": "patientswi", + "value": "3812" + }, + { + "label": "pps", + "value": "3760" + }, + { + "label": "pittree", + "value": "3728" + }, + { + "label": "penandinkw", + "value": "3675" + }, + { + "label": "Peachgift", + "value": "3671" + }, + { + "label": "Photograph", + "value": "3556" + }, + { + "label": "peopleflyi", + "value": "3522" + }, + { + "label": "PhantomThi", + "value": "3370" + }, + { + "label": "Peasant", + "value": "3303" + }, + { + "label": "PastandPre", + "value": "2994" + }, + { + "label": "Pretendtob", + "value": "2976" + }, + { + "label": "Pleaseforg", + "value": "2933" + }, + { + "label": "psionic", + "value": "2910" + }, + { + "label": "Protagonis", + "value": "2881" + }, + { + "label": "PrinceofHe", + "value": "2847" + }, + { + "label": "pendragon", + "value": "2827" + }, + { + "label": "PokémonGo", + "value": "2761" + }, + { + "label": "PiratesofH", + "value": "2748" + }, + { + "label": "potatogirl", + "value": "2729" + }, + { + "label": "Piscesinth", + "value": "2681" + }, + { + "label": "Papaisvery", + "value": "2647" + }, + { + "label": "Peoplenear", + "value": "2632" + }, + { + "label": "pigeonnext", + "value": "2599" + }, + { + "label": "PiratexFai", + "value": "2559" + }, + { + "label": "pureimpuls", + "value": "2538" + }, + { + "label": "petsurviva", + "value": "2511" + }, + { + "label": "PirateCour", + "value": "2497" + }, + { + "label": "PopeBibiDo", + "value": "2494" + }, + { + "label": "PokémonTi", + "value": "2492" + }, + { + "label": "PirateWars", + "value": "2491" + }, + { + "label": "Pirateacto", + "value": "2489" + }, + { + "label": "purekitten", + "value": "2465" + }, + { + "label": "paleandwhi", + "value": "2264" + }, + { + "label": "Pipifish", + "value": "2234" + }, + { + "label": "panic", + "value": "2166" + }, + { + "label": "plumthirte", + "value": "2156" + }, + { + "label": "PokémonVo", + "value": "2155" + }, + { + "label": "PirateGrea", + "value": "2147" + }, + { + "label": "Positiveel", + "value": "2098" + }, + { + "label": "pendreamst", + "value": "2096" + }, + { + "label": "PlayBlueMo", + "value": "2069" + }, + { + "label": "pleasantin", + "value": "2054" + }, + { + "label": "part-timeo", + "value": "2025" + }, + { + "label": "Punch", + "value": "1960" + }, + { + "label": "Piratehaha", + "value": "1914" + }, + { + "label": "PirateDaQi", + "value": "1844" + }, + { + "label": "perfectmag", + "value": "1803" + }, + { + "label": "Peerlessso", + "value": "1795" + }, + { + "label": "PeerlessSw", + "value": "1791" + }, + { + "label": "Poorcrazy", + "value": "1787" + }, + { + "label": "PresentDay", + "value": "1732" + }, + { + "label": "PlayerKill", + "value": "1690" + }, + { + "label": "Parasite", + "value": "1554" + }, + { + "label": "Psychology", + "value": "1544" + }, + { + "label": "PoorRoRich", + "value": "1535" + }, + { + "label": "Puzzles", + "value": "1529" + }, + { + "label": "Protagonis", + "value": "1525" + }, + { + "label": "PlayingGho", + "value": "1482" + }, + { + "label": "PositiveLe", + "value": "1455" + }, + { + "label": "poems", + "value": "1438" + }, + { + "label": "Partnerofa", + "value": "1366" + }, + { + "label": "Prehistori", + "value": "1362" + }, + { + "label": "Prehistori", + "value": "1352" + }, + { + "label": "Protagonis", + "value": "1341" + }, + { + "label": "Protagonis", + "value": "1305" + }, + { + "label": "PseudoReli", + "value": "1282" + }, + { + "label": "PseudoHolo", + "value": "1281" + }, + { + "label": "PoliticalS", + "value": "1279" + }, + { + "label": "Playboymal", + "value": "1275" + }, + { + "label": "Painter", + "value": "1262" + }, + { + "label": "Players", + "value": "1245" + }, + { + "label": "Painting", + "value": "1229" + }, + { + "label": "PacifistPr", + "value": "1186" + }, + { + "label": "Primitives", + "value": "1138" + }, + { + "label": "Plants", + "value": "1137" + }, + { + "label": "Protagonis", + "value": "1129" + }, + { + "label": "PoisonMout", + "value": "1070" + }, + { + "label": "Psychic", + "value": "1067" + }, + { + "label": "PowersTran", + "value": "1066" + }, + { + "label": "Priestesse", + "value": "1059" + }, + { + "label": "Planets", + "value": "1004" + }, + { + "label": "PoliticalI", + "value": "949" + }, + { + "label": "Professor", + "value": "928" + }, + { + "label": "ParalelWor", + "value": "823" + }, + { + "label": "Paizuri", + "value": "752" + }, + { + "label": "Photograph", + "value": "705" + }, + { + "label": "PamperingR", + "value": "591" + }, + { + "label": "PastPlaysa", + "value": "497" + }, + { + "label": "Protagonis", + "value": "462" + }, + { + "label": "Protagonis", + "value": "283" + }, + { + "label": "Persistent", + "value": "274" + }, + { + "label": "Protagonis", + "value": "49" + }, + { + "label": "Queen", + "value": "3574" + }, + { + "label": "QuickTrans", + "value": "599" + }, + { + "label": "QinHan", + "value": "3230" + }, + { + "label": "qidian", + "value": "5044" + }, + { + "label": "QingDynast", + "value": "3174" + }, + { + "label": "QuirkyChar", + "value": "198" + }, + { + "label": "qimao", + "value": "5120" + }, + { + "label": "QiLuck", + "value": "1641" + }, + { + "label": "Quickwear", + "value": "1547" + }, + { + "label": "QinBichu", + "value": "1859" + }, + { + "label": "quietflowe", + "value": "1847" + }, + { + "label": "QuickTrans", + "value": "944" + }, + { + "label": "QuietChara", + "value": "719" + }, + { + "label": "清穿", + "value": "6197" + }, + { + "label": "群穿", + "value": "6192" + }, + { + "label": "qhfishinga", + "value": "5981" + }, + { + "label": "QianandQia", + "value": "5868" + }, + { + "label": "Qingsanren", + "value": "5820" + }, + { + "label": "QiyueLiuhu", + "value": "5730" + }, + { + "label": "Qiandengwi", + "value": "5721" + }, + { + "label": "QinTangFei", + "value": "5610" + }, + { + "label": "Qiyao&0", + "value": "5603" + }, + { + "label": "Quackhamst", + "value": "5601" + }, + { + "label": "QinShiSwor", + "value": "5569" + }, + { + "label": "Quasar", + "value": "5568" + }, + { + "label": "QuartetSev", + "value": "5486" + }, + { + "label": "quantumgoo", + "value": "5396" + }, + { + "label": "QingWeixin", + "value": "5368" + }, + { + "label": "QinghuiYen", + "value": "5303" + }, + { + "label": "QingfengYu", + "value": "4864" + }, + { + "label": "Qingxinyuu", + "value": "4852" + }, + { + "label": "QiYuwholov", + "value": "4817" + }, + { + "label": "QiyueShiqi", + "value": "4752" + }, + { + "label": "Qianqianra", + "value": "4582" + }, + { + "label": "QinHanTang", + "value": "4447" + }, + { + "label": "QinBuxiang", + "value": "4289" + }, + { + "label": "Qilixiang", + "value": "4045" + }, + { + "label": "QiuZiXiaXu", + "value": "3995" + }, + { + "label": "QianXixi", + "value": "3718" + }, + { + "label": "Qingluan", + "value": "3693" + }, + { + "label": "QianXunxia", + "value": "3683" + }, + { + "label": "Qianlong", + "value": "3645" + }, + { + "label": "QinHanCros", + "value": "3039" + }, + { + "label": "QingDynast", + "value": "2993" + }, + { + "label": "清裁", + "value": "2838" + }, + { + "label": "QianshanTw", + "value": "2735" + }, + { + "label": "Quququ", + "value": "2613" + }, + { + "label": "QueenofBla", + "value": "2445" + }, + { + "label": "qingyu", + "value": "2360" + }, + { + "label": "QiXuan", + "value": "2111" + }, + { + "label": "QingheTaoi", + "value": "2032" + }, + { + "label": "Qingliansw", + "value": "1970" + }, + { + "label": "Quasi-GodS", + "value": "1919" + }, + { + "label": "Qingfeng1D", + "value": "1853" + }, + { + "label": "QT", + "value": "1758" + }, + { + "label": "Question&a", + "value": "1728" + }, + { + "label": "QuickTrans", + "value": "1659" + }, + { + "label": "QuickPass", + "value": "1621" + }, + { + "label": "relaxed", + "value": "1761" + }, + { + "label": "Rebirth", + "value": "139" + }, + { + "label": "Reincarnat", + "value": "162" + }, + { + "label": "Revenge", + "value": "225" + }, + { + "label": "Romance", + "value": "768" + }, + { + "label": "Reunion", + "value": "1487" + }, + { + "label": "R-13", + "value": "727" + }, + { + "label": "Returnofth", + "value": "3236" + }, + { + "label": "RomanticSu", + "value": "14" + }, + { + "label": "RuthlessPr", + "value": "15" + }, + { + "label": "RoyalBeast", + "value": "3131" + }, + { + "label": "ReikiRecov", + "value": "1765" + }, + { + "label": "Racism", + "value": "13" + }, + { + "label": "Royalty", + "value": "35" + }, + { + "label": "R18", + "value": "5058" + }, + { + "label": "Raiders", + "value": "3250" + }, + { + "label": "RebirthedP", + "value": "1241" + }, + { + "label": "Regret", + "value": "3832" + }, + { + "label": "rejoice", + "value": "3228" + }, + { + "label": "Regression", + "value": "3835" + }, + { + "label": "roughman", + "value": "3397" + }, + { + "label": "Royal", + "value": "3354" + }, + { + "label": "Rpe", + "value": "5" + }, + { + "label": "Reverse", + "value": "3390" + }, + { + "label": "Rape", + "value": "402" + }, + { + "label": "reversewea", + "value": "3256" + }, + { + "label": "richandbea", + "value": "3454" + }, + { + "label": "Remarriage", + "value": "3572" + }, + { + "label": "R-15", + "value": "646" + }, + { + "label": "RedHouse", + "value": "3187" + }, + { + "label": "ReverseHar", + "value": "378" + }, + { + "label": "Refiner", + "value": "3155" + }, + { + "label": "redpacketf", + "value": "3711" + }, + { + "label": "richpeople", + "value": "3395" + }, + { + "label": "Religions", + "value": "706" + }, + { + "label": "Rollover", + "value": "3457" + }, + { + "label": "Resurrecti", + "value": "390" + }, + { + "label": "Richestman", + "value": "3715" + }, + { + "label": "reincarnat", + "value": "3438" + }, + { + "label": "reincarnat", + "value": "1399" + }, + { + "label": "RighteousP", + "value": "630" + }, + { + "label": "Regent", + "value": "3233" + }, + { + "label": "RomanceFan", + "value": "3833" + }, + { + "label": "rawstream", + "value": "3449" + }, + { + "label": "RichProtag", + "value": "875" + }, + { + "label": "ReverseRpe", + "value": "614" + }, + { + "label": "Reborn", + "value": "831" + }, + { + "label": "RaceChange", + "value": "604" + }, + { + "label": "rejoiceine", + "value": "6188" + }, + { + "label": "RuneMaster", + "value": "3365" + }, + { + "label": "Rivalry", + "value": "687" + }, + { + "label": "rural", + "value": "3696" + }, + { + "label": "riot", + "value": "3604" + }, + { + "label": "Rideawhale", + "value": "1821" + }, + { + "label": "Rich", + "value": "1051" + }, + { + "label": "ReverseRap", + "value": "571" + }, + { + "label": "Reunionaft", + "value": "6191" + }, + { + "label": "Reversal", + "value": "5131" + }, + { + "label": "readstream", + "value": "3205" + }, + { + "label": "Reikyrecov", + "value": "1054" + }, + { + "label": "RankSystem", + "value": "997" + }, + { + "label": "Roommates", + "value": "772" + }, + { + "label": "Restaurant", + "value": "635" + }, + { + "label": "randomstre", + "value": "5631" + }, + { + "label": "R-18", + "value": "5455" + }, + { + "label": "rivercruis", + "value": "5161" + }, + { + "label": "Rarebloodl", + "value": "5060" + }, + { + "label": "Raven", + "value": "4468" + }, + { + "label": "Redemption", + "value": "3837" + }, + { + "label": "riversande", + "value": "2011" + }, + { + "label": "RimuruTemp", + "value": "1593" + }, + { + "label": "rwby", + "value": "1406" + }, + { + "label": "Richfamily", + "value": "1388" + }, + { + "label": "RuthlessMc", + "value": "1093" + }, + { + "label": "RichCharac", + "value": "910" + }, + { + "label": "RichtoPoor", + "value": "743" + }, + { + "label": "Reporters", + "value": "545" + }, + { + "label": "RanchFarmi", + "value": "6182" + }, + { + "label": "Rideawhale", + "value": "6063" + }, + { + "label": "RebirthofK", + "value": "6033" + }, + { + "label": "rideapiggy", + "value": "5975" + }, + { + "label": "Rememberto", + "value": "5928" + }, + { + "label": "Regardless", + "value": "5922" + }, + { + "label": "Rebellious", + "value": "5906" + }, + { + "label": "reasoning", + "value": "5891" + }, + { + "label": "Returntent", + "value": "5778" + }, + { + "label": "Rhapsodyof", + "value": "5748" + }, + { + "label": "RED⑨", + "value": "5729" + }, + { + "label": "rainclearn", + "value": "5634" + }, + { + "label": "Return", + "value": "5630" + }, + { + "label": "reallynotg", + "value": "5624" + }, + { + "label": "rodeaway", + "value": "5623" + }, + { + "label": "redstone", + "value": "5572" + }, + { + "label": "realoldgen", + "value": "5552" + }, + { + "label": "RegalinBla", + "value": "5549" + }, + { + "label": "Ridethewin", + "value": "5537" + }, + { + "label": "rainyday", + "value": "5480" + }, + { + "label": "realnamest", + "value": "5433" + }, + { + "label": "ruotuosave", + "value": "5415" + }, + { + "label": "Rehabilita", + "value": "5412" + }, + { + "label": "rakepig", + "value": "5398" + }, + { + "label": "Returnofth", + "value": "5378" + }, + { + "label": "randomwhy", + "value": "5373" + }, + { + "label": "revengeLov", + "value": "5345" + }, + { + "label": "Rebirthswe", + "value": "5328" + }, + { + "label": "reversewea", + "value": "5325" + }, + { + "label": "Rebirthstr", + "value": "5318" + }, + { + "label": "RebirthMen", + "value": "5311" + }, + { + "label": "Rebirthsho", + "value": "5306" + }, + { + "label": "Rhein-Life", + "value": "5250" + }, + { + "label": "RipplesofD", + "value": "5220" + }, + { + "label": "Recallingt", + "value": "5217" + }, + { + "label": "Rossini", + "value": "5167" + }, + { + "label": "rockblosso", + "value": "5156" + }, + { + "label": "Rainink", + "value": "5154" + }, + { + "label": "ReformandO", + "value": "5129" + }, + { + "label": "Realestate", + "value": "5122" + }, + { + "label": "runaftercl", + "value": "5104" + }, + { + "label": "ricestar", + "value": "5102" + }, + { + "label": "raccoonrac", + "value": "5028" + }, + { + "label": "RepeatJiAi", + "value": "5027" + }, + { + "label": "RedLotusWi", + "value": "5008" + }, + { + "label": "runawaydum", + "value": "4979" + }, + { + "label": "RoadtoAwak", + "value": "4970" + }, + { + "label": "Redactor", + "value": "4908" + }, + { + "label": "rainystars", + "value": "4892" + }, + { + "label": "Repeat", + "value": "4876" + }, + { + "label": "ridicule", + "value": "4860" + }, + { + "label": "ricebath", + "value": "4850" + }, + { + "label": "runawayrab", + "value": "4792" + }, + { + "label": "RongRong", + "value": "4787" + }, + { + "label": "RomeoTanak", + "value": "4766" + }, + { + "label": "RedIronRoa", + "value": "4755" + }, + { + "label": "redcutefoo", + "value": "4727" + }, + { + "label": "respectfor", + "value": "4657" + }, + { + "label": "rememberto", + "value": "4640" + }, + { + "label": "Referstomu", + "value": "4601" + }, + { + "label": "Rainandsun", + "value": "4537" + }, + { + "label": "Roger18", + "value": "4504" + }, + { + "label": "Railgun", + "value": "4490" + }, + { + "label": "Rejoiceint", + "value": "4446" + }, + { + "label": "rainiscomi", + "value": "4368" + }, + { + "label": "Rarely", + "value": "4362" + }, + { + "label": "Reincarnat", + "value": "4356" + }, + { + "label": "Rengucuisi", + "value": "4235" + }, + { + "label": "runawaymid", + "value": "4217" + }, + { + "label": "Rainhitsba", + "value": "4164" + }, + { + "label": "Raynes", + "value": "4157" + }, + { + "label": "Rainfallsf", + "value": "4104" + }, + { + "label": "Reimuer", + "value": "4017" + }, + { + "label": "Ranwho", + "value": "3969" + }, + { + "label": "RavenCrow", + "value": "3959" + }, + { + "label": "Rococo", + "value": "3924" + }, + { + "label": "Rushduck", + "value": "3922" + }, + { + "label": "Reiki", + "value": "3859" + }, + { + "label": "raccoonsho", + "value": "3797" + }, + { + "label": "recessiono", + "value": "3796" + }, + { + "label": "rottenoran", + "value": "3781" + }, + { + "label": "rainbeans", + "value": "3769" + }, + { + "label": "ridiculous", + "value": "3744" + }, + { + "label": "rockfox", + "value": "3720" + }, + { + "label": "runningfis", + "value": "3690" + }, + { + "label": "Redcandyso", + "value": "3689" + }, + { + "label": "redlotus", + "value": "3576" + }, + { + "label": "ridethewin", + "value": "3493" + }, + { + "label": "RomanHolid", + "value": "3489" + }, + { + "label": "RiseofGras", + "value": "3437" + }, + { + "label": "reversewea", + "value": "3080" + }, + { + "label": "RebirthRel", + "value": "3048" + }, + { + "label": "Returnofth", + "value": "3042" + }, + { + "label": "Rebirthfar", + "value": "3034" + }, + { + "label": "Rebirthstr", + "value": "3032" + }, + { + "label": "RebirthGam", + "value": "3025" + }, + { + "label": "Rebirthint", + "value": "3019" + }, + { + "label": "Rebirthevo", + "value": "3017" + }, + { + "label": "Rebirtheve", + "value": "3014" + }, + { + "label": "RebirthGra", + "value": "3008" + }, + { + "label": "Rebirthman", + "value": "2988" + }, + { + "label": "Rebirthdoc", + "value": "2987" + }, + { + "label": "RebirthChr", + "value": "2984" + }, + { + "label": "Rebirthstr", + "value": "2975" + }, + { + "label": "Rebirthhap", + "value": "2936" + }, + { + "label": "Residentev", + "value": "2887" + }, + { + "label": "Realmmonst", + "value": "2810" + }, + { + "label": "rainydaywi", + "value": "2788" + }, + { + "label": "Resurrecti", + "value": "2701" + }, + { + "label": "rainboweig", + "value": "2682" + }, + { + "label": "Rotaryhotp", + "value": "2627" + }, + { + "label": "Raiseaghos", + "value": "2611" + }, + { + "label": "raisedache", + "value": "2600" + }, + { + "label": "Roon", + "value": "2557" + }, + { + "label": "RabbitToot", + "value": "2539" + }, + { + "label": "richeveryy", + "value": "2510" + }, + { + "label": "Residencen", + "value": "2400" + }, + { + "label": "recreation", + "value": "2328" + }, + { + "label": "restaurant", + "value": "2290" + }, + { + "label": "reallyking", + "value": "2284" + }, + { + "label": "rainandsno", + "value": "2254" + }, + { + "label": "Ruoshuithr", + "value": "2241" + }, + { + "label": "Rapeseedra", + "value": "2159" + }, + { + "label": "reversesmo", + "value": "2050" + }, + { + "label": "RoyalSabur", + "value": "1957" + }, + { + "label": "runawayant", + "value": "1841" + }, + { + "label": "runawaycit", + "value": "1809" + }, + { + "label": "RinYueqing", + "value": "1792" + }, + { + "label": "Russian", + "value": "1766" + }, + { + "label": "RankingLis", + "value": "1743" + }, + { + "label": "reincarnat", + "value": "1737" + }, + { + "label": "Researcher", + "value": "1701" + }, + { + "label": "Ras", + "value": "1688" + }, + { + "label": "Regressor", + "value": "1563" + }, + { + "label": "Reincarnat", + "value": "1444" + }, + { + "label": "ruthelessm", + "value": "1435" + }, + { + "label": "revenge", + "value": "1398" + }, + { + "label": "RookieProt", + "value": "1358" + }, + { + "label": "Rune", + "value": "1244" + }, + { + "label": "Rebornprot", + "value": "1239" + }, + { + "label": "RichMC", + "value": "1144" + }, + { + "label": "Returntoth", + "value": "1139" + }, + { + "label": "ReligiousO", + "value": "1057" + }, + { + "label": "ResolutePr", + "value": "1034" + }, + { + "label": "Reincarnat", + "value": "1028" + }, + { + "label": "Raids", + "value": "971" + }, + { + "label": "ReligousOr", + "value": "923" + }, + { + "label": "Reincarnat", + "value": "913" + }, + { + "label": "RefiningPi", + "value": "853" + }, + { + "label": "RomanticPr", + "value": "842" + }, + { + "label": "RapeVictim", + "value": "827" + }, + { + "label": "RedAlert2", + "value": "812" + }, + { + "label": "Reincarnat", + "value": "770" + }, + { + "label": "Rebellion", + "value": "757" + }, + { + "label": "Reversible", + "value": "740" + }, + { + "label": "RomanticSu", + "value": "711" + }, + { + "label": "Returningf", + "value": "695" + }, + { + "label": "Reincarnat", + "value": "523" + }, + { + "label": "Reincarnat", + "value": "467" + }, + { + "label": "Reincarnat", + "value": "415" + }, + { + "label": "RpeVictimB", + "value": "115" + }, + { + "label": "RapeVictim", + "value": "6" + }, + { + "label": "SystemFlow", + "value": "1644" + }, + { + "label": "strongfema", + "value": "3102" + }, + { + "label": "sweetpet", + "value": "3105" + }, + { + "label": "System", + "value": "119" + }, + { + "label": "Shuangwen", + "value": "1207" + }, + { + "label": "Slap", + "value": "3135" + }, + { + "label": "shortforpa", + "value": "3175" + }, + { + "label": "Strong", + "value": "1271" + }, + { + "label": "streamofhe", + "value": "3130" + }, + { + "label": "Strategy", + "value": "3109" + }, + { + "label": "Shuangjie", + "value": "3148" + }, + { + "label": "SystemAdmi", + "value": "31" + }, + { + "label": "SpecialAbi", + "value": "353" + }, + { + "label": "SecondChan", + "value": "99" + }, + { + "label": "Showbiz", + "value": "108" + }, + { + "label": "SingleHero", + "value": "1569" + }, + { + "label": "神话", + "value": "3222" + }, + { + "label": "Student", + "value": "3208" + }, + { + "label": "scumbag", + "value": "3114" + }, + { + "label": "Self-disci", + "value": "3207" + }, + { + "label": "Suspensefl", + "value": "3363" + }, + { + "label": "SwordAndMa", + "value": "288" + }, + { + "label": "Summoningf", + "value": "3156" + }, + { + "label": "SuperA", + "value": "3252" + }, + { + "label": "StraightAs", + "value": "3286" + }, + { + "label": "Solitarype", + "value": "3243" + }, + { + "label": "Space-time", + "value": "3178" + }, + { + "label": "StrongtoSt", + "value": "118" + }, + { + "label": "SlowRomanc", + "value": "144" + }, + { + "label": "seeyouagai", + "value": "3116" + }, + { + "label": "Sanguanzhe", + "value": "3197" + }, + { + "label": "Sign-InChe", + "value": "1242" + }, + { + "label": "Survival", + "value": "100" + }, + { + "label": "Shurachang", + "value": "3291" + }, + { + "label": "schoolgras", + "value": "3615" + }, + { + "label": "signin", + "value": "1294" + }, + { + "label": "Saltyandsw", + "value": "3314" + }, + { + "label": "SealGod", + "value": "3399" + }, + { + "label": "sick", + "value": "3323" + }, + { + "label": "Suspensefu", + "value": "3345" + }, + { + "label": "Shijia", + "value": "3244" + }, + { + "label": "Streamwith", + "value": "3241" + }, + { + "label": "ShamelessP", + "value": "203" + }, + { + "label": "StrongLove", + "value": "30" + }, + { + "label": "supremekin", + "value": "5698" + }, + { + "label": "struggle", + "value": "3326" + }, + { + "label": "strongwoma", + "value": "4349" + }, + { + "label": "Sports", + "value": "992" + }, + { + "label": "Song", + "value": "5695" + }, + { + "label": "straightma", + "value": "3203" + }, + { + "label": "Secretive", + "value": "5066" + }, + { + "label": "self-impro", + "value": "3306" + }, + { + "label": "Sisterandb", + "value": "3358" + }, + { + "label": "SuperTechn", + "value": "1243" + }, + { + "label": "swordrepai", + "value": "3280" + }, + { + "label": "SliceofLif", + "value": "877" + }, + { + "label": "Scientists", + "value": "486" + }, + { + "label": "SummoningM", + "value": "315" + }, + { + "label": "SwordWield", + "value": "325" + }, + { + "label": "SemeProtag", + "value": "500" + }, + { + "label": "Superpower", + "value": "360" + }, + { + "label": "SurvivalGa", + "value": "514" + }, + { + "label": "Strongfrom", + "value": "306" + }, + { + "label": "sweetwife", + "value": "3563" + }, + { + "label": "Startabusi", + "value": "3325" + }, + { + "label": "Son-in-law", + "value": "1461" + }, + { + "label": "SisterYu", + "value": "3184" + }, + { + "label": "Strategist", + "value": "451" + }, + { + "label": "strongstre", + "value": "3347" + }, + { + "label": "softricefl", + "value": "3308" + }, + { + "label": "StrongProt", + "value": "1212" + }, + { + "label": "SecretIden", + "value": "285" + }, + { + "label": "SpecialFor", + "value": "1327" + }, + { + "label": "sweetheart", + "value": "5688" + }, + { + "label": "substitute", + "value": "3567" + }, + { + "label": "ShoujoAi", + "value": "955" + }, + { + "label": "SlowGrowth", + "value": "487" + }, + { + "label": "Smalldogs", + "value": "3592" + }, + { + "label": "Summoner", + "value": "938" + }, + { + "label": "Saltedfish", + "value": "3292" + }, + { + "label": "SuiandTang", + "value": "3392" + }, + { + "label": "Soccer", + "value": "511" + }, + { + "label": "sweetandco", + "value": "5681" + }, + { + "label": "Spirits", + "value": "555" + }, + { + "label": "宋朝", + "value": "3124" + }, + { + "label": "supplier", + "value": "3602" + }, + { + "label": "Slaves", + "value": "287" + }, + { + "label": "space-time", + "value": "5694" + }, + { + "label": "Shiko", + "value": "3367" + }, + { + "label": "Sportscomp", + "value": "3706" + }, + { + "label": "stranger", + "value": "3211" + }, + { + "label": "StrongBack", + "value": "1324" + }, + { + "label": "SweetText", + "value": "929" + }, + { + "label": "SmartCoupl", + "value": "513" + }, + { + "label": "SuddenWeal", + "value": "660" + }, + { + "label": "SerpentKin", + "value": "3389" + }, + { + "label": "StrategicB", + "value": "150" + }, + { + "label": "Spaceship", + "value": "547" + }, + { + "label": "soulconver", + "value": "3589" + }, + { + "label": "Shushan", + "value": "3284" + }, + { + "label": "Steampunk", + "value": "896" + }, + { + "label": "Singers", + "value": "187" + }, + { + "label": "Space", + "value": "917" + }, + { + "label": "SchoolLife", + "value": "746" + }, + { + "label": "Souls", + "value": "449" + }, + { + "label": "SkillAssim", + "value": "403" + }, + { + "label": "slackerstu", + "value": "3713" + }, + { + "label": "ShanHaiJin", + "value": "3396" + }, + { + "label": "Sects", + "value": "881" + }, + { + "label": "StoreOwner", + "value": "322" + }, + { + "label": "SecretOrga", + "value": "1141" + }, + { + "label": "sciencefic", + "value": "1046" + }, + { + "label": "Survivalch", + "value": "3344" + }, + { + "label": "Supernatur", + "value": "820" + }, + { + "label": "SicklyChar", + "value": "600" + }, + { + "label": "SkillCreat", + "value": "572" + }, + { + "label": "Sadomasoch", + "value": "5677" + }, + { + "label": "SupremeStr", + "value": "3640" + }, + { + "label": "spookygame", + "value": "3570" + }, + { + "label": "secretivec", + "value": "3085" + }, + { + "label": "Sci-fi", + "value": "931" + }, + { + "label": "Swordsman", + "value": "793" + }, + { + "label": "SentientSk", + "value": "1313" + }, + { + "label": "SectDevelo", + "value": "692" + }, + { + "label": "SecretOrga", + "value": "652" + }, + { + "label": "sillywhite", + "value": "3776" + }, + { + "label": "showdownfl", + "value": "3598" + }, + { + "label": "Spywar", + "value": "3186" + }, + { + "label": "SlowCultiv", + "value": "1009" + }, + { + "label": "SkillBooks", + "value": "508" + }, + { + "label": "SoulPower", + "value": "404" + }, + { + "label": "spoiler", + "value": "3588" + }, + { + "label": "Smut", + "value": "975" + }, + { + "label": "ShortStory", + "value": "559" + }, + { + "label": "SuddenStre", + "value": "551" + }, + { + "label": "Son-in-law", + "value": "6166" + }, + { + "label": "Salvation", + "value": "3846" + }, + { + "label": "Securitygu", + "value": "3320" + }, + { + "label": "SpaceOpera", + "value": "807" + }, + { + "label": "SummonedHe", + "value": "602" + }, + { + "label": "SinglePare", + "value": "503" + }, + { + "label": "Soldiers", + "value": "455" + }, + { + "label": "Senior", + "value": "3318" + }, + { + "label": "Shota", + "value": "493" + }, + { + "label": "Secrets", + "value": "286" + }, + { + "label": "SexualAbus", + "value": "175" + }, + { + "label": "system", + "value": "1409" + }, + { + "label": "Shounen-Ai", + "value": "530" + }, + { + "label": "SealedPowe", + "value": "284" + }, + { + "label": "Siblings", + "value": "48" + }, + { + "label": "Sweet", + "value": "1158" + }, + { + "label": "Shoujo-AiS", + "value": "595" + }, + { + "label": "SerialKill", + "value": "550" + }, + { + "label": "SlaveProta", + "value": "540" + }, + { + "label": "SelfishPro", + "value": "254" + }, + { + "label": "smartprota", + "value": "1215" + }, + { + "label": "Simulator", + "value": "1172" + }, + { + "label": "Schemesand", + "value": "1047" + }, + { + "label": "SmartMC", + "value": "937" + }, + { + "label": "SisterComp", + "value": "709" + }, + { + "label": "StraightUk", + "value": "438" + }, + { + "label": "Shapeshift", + "value": "267" + }, + { + "label": "SigninChec", + "value": "1331" + }, + { + "label": "SuperHeroe", + "value": "1130" + }, + { + "label": "SentientOb", + "value": "970" + }, + { + "label": "StrongFema", + "value": "861" + }, + { + "label": "Suicides", + "value": "775" + }, + { + "label": "SavingtheW", + "value": "682" + }, + { + "label": "SpiritAdvi", + "value": "654" + }, + { + "label": "SpearWield", + "value": "578" + }, + { + "label": "SelflessPr", + "value": "558" + }, + { + "label": "ShyCharact", + "value": "482" + }, + { + "label": "SexSlaves", + "value": "475" + }, + { + "label": "SpiritUser", + "value": "450" + }, + { + "label": "SecretCrus", + "value": "266" + }, + { + "label": "SkeletonSo", + "value": "3440" + }, + { + "label": "straightma", + "value": "2893" + }, + { + "label": "Summons", + "value": "1475" + }, + { + "label": "StrongCoup", + "value": "1380" + }, + { + "label": "Slice-of-l", + "value": "1259" + }, + { + "label": "StrongMC", + "value": "889" + }, + { + "label": "Seduction", + "value": "732" + }, + { + "label": "StoicChara", + "value": "729" + }, + { + "label": "Succubus", + "value": "681" + }, + { + "label": "Servants", + "value": "665" + }, + { + "label": "StubbornPr", + "value": "565" + }, + { + "label": "SecretiveP", + "value": "552" + }, + { + "label": "StockholmS", + "value": "188" + }, + { + "label": "storytelle", + "value": "5117" + }, + { + "label": "smallblade", + "value": "4734" + }, + { + "label": "sixteennig", + "value": "4516" + }, + { + "label": "SaraDouble", + "value": "3810" + }, + { + "label": "Swordgod", + "value": "2579" + }, + { + "label": "spendthewo", + "value": "2049" + }, + { + "label": "Shounen", + "value": "1533" + }, + { + "label": "Slave", + "value": "1463" + }, + { + "label": "Superpower", + "value": "1265" + }, + { + "label": "systemowne", + "value": "1240" + }, + { + "label": "SCP", + "value": "1140" + }, + { + "label": "Sign-in", + "value": "934" + }, + { + "label": "summon", + "value": "803" + }, + { + "label": "Saves", + "value": "781" + }, + { + "label": "SpatialMan", + "value": "721" + }, + { + "label": "SadisticCh", + "value": "622" + }, + { + "label": "SpecialAbi", + "value": "347" + }, + { + "label": "SonofYuyin", + "value": "5883" + }, + { + "label": "Sako", + "value": "5754" + }, + { + "label": "Stand-alon", + "value": "5130" + }, + { + "label": "southmirro", + "value": "5013" + }, + { + "label": "Saltedfish", + "value": "4874" + }, + { + "label": "SixImmorta", + "value": "4796" + }, + { + "label": "Shiewillno", + "value": "4764" + }, + { + "label": "Sora", + "value": "4420" + }, + { + "label": "Sherlock", + "value": "4300" + }, + { + "label": "Saltedfish", + "value": "4233" + }, + { + "label": "stallion", + "value": "4149" + }, + { + "label": "ShoesMenlu", + "value": "4086" + }, + { + "label": "saltedfish", + "value": "4074" + }, + { + "label": "sickleredr", + "value": "3861" + }, + { + "label": "sixteenmir", + "value": "3783" + }, + { + "label": "supergodfi", + "value": "3753" + }, + { + "label": "Sadakoiscu", + "value": "3672" + }, + { + "label": "sunsetmoon", + "value": "3476" + }, + { + "label": "Snow", + "value": "3471" + }, + { + "label": "Satire", + "value": "3420" + }, + { + "label": "soulmemory", + "value": "2515" + }, + { + "label": "SixPathsof", + "value": "2449" + }, + { + "label": "Saltedfish", + "value": "2405" + }, + { + "label": "startwriti", + "value": "2121" + }, + { + "label": "smallninel", + "value": "1900" + }, + { + "label": "Sterile", + "value": "1862" + }, + { + "label": "Stayupalln", + "value": "1813" + }, + { + "label": "secretary", + "value": "1733" + }, + { + "label": "SecondChan", + "value": "1665" + }, + { + "label": "Simulation", + "value": "1646" + }, + { + "label": "Sea", + "value": "1570" + }, + { + "label": "SlowLife", + "value": "1481" + }, + { + "label": "ShouProtag", + "value": "1467" + }, + { + "label": "Star", + "value": "1302" + }, + { + "label": "StrongLove", + "value": "1224" + }, + { + "label": "Suspense", + "value": "1213" + }, + { + "label": "Superstar", + "value": "1147" + }, + { + "label": "Shapeshift", + "value": "1100" + }, + { + "label": "SaintSeiya", + "value": "1082" + }, + { + "label": "SaikiK", + "value": "1064" + }, + { + "label": "Samurai", + "value": "1025" + }, + { + "label": "SecretRela", + "value": "1021" + }, + { + "label": "SportsBask", + "value": "993" + }, + { + "label": "SpecialLik", + "value": "940" + }, + { + "label": "SevenDeadl", + "value": "691" + }, + { + "label": "StraightSe", + "value": "643" + }, + { + "label": "Saints", + "value": "598" + }, + { + "label": "Shotacon", + "value": "556" + }, + { + "label": "Spies", + "value": "251" + }, + { + "label": "SxualAbuse", + "value": "116" + }, + { + "label": "SiblingRiv", + "value": "46" + }, + { + "label": "strongandm", + "value": "6201" + }, + { + "label": "Supporting", + "value": "6189" + }, + { + "label": "双洁", + "value": "6178" + }, + { + "label": "Sciencefic", + "value": "6161" + }, + { + "label": "Sectbuildi", + "value": "6158" + }, + { + "label": "Sandsculpt", + "value": "6142" + }, + { + "label": "肆灵", + "value": "6123" + }, + { + "label": "ShamelessA", + "value": "6108" + }, + { + "label": "SirBirefel", + "value": "6090" + }, + { + "label": "sunset", + "value": "6048" + }, + { + "label": "Smokeandra", + "value": "6038" + }, + { + "label": "Superman7", + "value": "6028" + }, + { + "label": "沙酱", + "value": "6027" + }, + { + "label": "Sixcauseso", + "value": "6026" + }, + { + "label": "sterlingsi", + "value": "5986" + }, + { + "label": "Smallcardd", + "value": "5982" + }, + { + "label": "SmallRibs", + "value": "5978" + }, + { + "label": "Saltedfish", + "value": "5959" + }, + { + "label": "snowfall", + "value": "5937" + }, + { + "label": "sea​​rou", + "value": "5930" + }, + { + "label": "SevenProfo", + "value": "5929" + }, + { + "label": "Strangerss", + "value": "5904" + }, + { + "label": "Saltedfish", + "value": "5893" + }, + { + "label": "SongofNort", + "value": "5861" + }, + { + "label": "startingze", + "value": "5859" + }, + { + "label": "Saltedfish", + "value": "5852" + }, + { + "label": "scarletmon", + "value": "5814" + }, + { + "label": "SaltedFish", + "value": "5811" + }, + { + "label": "squirrelth", + "value": "5809" + }, + { + "label": "StormyMoon", + "value": "5806" + }, + { + "label": "soisthewin", + "value": "5797" + }, + { + "label": "saltedfish", + "value": "5794" + }, + { + "label": "stopsleepi", + "value": "5765" + }, + { + "label": "Supporting", + "value": "5682" + }, + { + "label": "SiheyuanDa", + "value": "5663" + }, + { + "label": "SoulWorldR", + "value": "5654" + }, + { + "label": "simpletwo", + "value": "5648" + }, + { + "label": "saturdaymo", + "value": "5645" + }, + { + "label": "Shaohuaisf", + "value": "5639" + }, + { + "label": "snowmoonco", + "value": "5618" + }, + { + "label": "sixlittlef", + "value": "5609" + }, + { + "label": "sellwine", + "value": "5606" + }, + { + "label": "Saint丨Men", + "value": "5593" + }, + { + "label": "SweetSoyMi", + "value": "5591" + }, + { + "label": "slagfly", + "value": "5589" + }, + { + "label": "sixchiefs", + "value": "5586" + }, + { + "label": "sendapacka", + "value": "5585" + }, + { + "label": "supernice1", + "value": "5584" + }, + { + "label": "Shuttlebus", + "value": "5577" + }, + { + "label": "Slimeatsun", + "value": "5576" + }, + { + "label": "SourceTaie", + "value": "5556" + }, + { + "label": "sleepingra", + "value": "5521" + }, + { + "label": "SakeAsahi", + "value": "5518" + }, + { + "label": "Southwindo", + "value": "5515" + }, + { + "label": "singlemons", + "value": "5469" + }, + { + "label": "Swordsheep", + "value": "5464" + }, + { + "label": "Snowballfi", + "value": "5442" + }, + { + "label": "scrupulous", + "value": "5432" + }, + { + "label": "silentauth", + "value": "5423" + }, + { + "label": "Supporting", + "value": "5400" + }, + { + "label": "SuzuharaYu", + "value": "5383" + }, + { + "label": "Sufferingp", + "value": "5376" + }, + { + "label": "stareather", + "value": "5371" + }, + { + "label": "sellbloodf", + "value": "5357" + }, + { + "label": "summerrain", + "value": "5355" + }, + { + "label": "Sauce", + "value": "5348" + }, + { + "label": "strongfema", + "value": "5339" + }, + { + "label": "strongfema", + "value": "5331" + }, + { + "label": "Slapstrong", + "value": "5313" + }, + { + "label": "ShuangjieD", + "value": "5312" + }, + { + "label": "superround", + "value": "5281" + }, + { + "label": "string", + "value": "5277" + }, + { + "label": "StewedRice", + "value": "5272" + }, + { + "label": "SMPZ", + "value": "5268" + }, + { + "label": "scumfish", + "value": "5262" + }, + { + "label": "SurfProsec", + "value": "5251" + }, + { + "label": "sure", + "value": "5243" + }, + { + "label": "secludedci", + "value": "5221" + }, + { + "label": "SanyueFeng", + "value": "5215" + }, + { + "label": "Southofthe", + "value": "5211" + }, + { + "label": "sanctifica", + "value": "5209" + }, + { + "label": "Scorpionho", + "value": "5193" + }, + { + "label": "silverbigs", + "value": "5178" + }, + { + "label": "Speechless", + "value": "5174" + }, + { + "label": "smallhalft", + "value": "5162" + }, + { + "label": "SillyTeres", + "value": "5145" + }, + { + "label": "scholar", + "value": "5119" + }, + { + "label": "Stealtheop", + "value": "5116" + }, + { + "label": "Stubbornan", + "value": "5108" + }, + { + "label": "smallself", + "value": "5107" + }, + { + "label": "Sweetlove", + "value": "5086" + }, + { + "label": "Scary", + "value": "5083" + }, + { + "label": "SuperAbili", + "value": "5067" + }, + { + "label": "specialpow", + "value": "5051" + }, + { + "label": "Stir-Fried", + "value": "5033" + }, + { + "label": "Siriuslook", + "value": "5003" + }, + { + "label": "SuFengqian", + "value": "4999" + }, + { + "label": "secretolds", + "value": "4974" + }, + { + "label": "SkyKilling", + "value": "4962" + }, + { + "label": "southwindt", + "value": "4959" + }, + { + "label": "SancheonHo", + "value": "4938" + }, + { + "label": "Spiritstri", + "value": "4935" + }, + { + "label": "smellylitt", + "value": "4931" + }, + { + "label": "Scorpio", + "value": "4914" + }, + { + "label": "slowlytwis", + "value": "4910" + }, + { + "label": "Slagwritin", + "value": "4903" + }, + { + "label": "starnightf", + "value": "4895" + }, + { + "label": "SCaja", + "value": "4889" + }, + { + "label": "stealingme", + "value": "4867" + }, + { + "label": "sleepingbi", + "value": "4847" + }, + { + "label": "snowmanbef", + "value": "4833" + }, + { + "label": "summerseaf", + "value": "4828" + }, + { + "label": "Saltedfish", + "value": "4818" + }, + { + "label": "smallten", + "value": "4814" + }, + { + "label": "ShangshanX", + "value": "4813" + }, + { + "label": "SunnyPiggy", + "value": "4782" + }, + { + "label": "SparklingP", + "value": "4757" + }, + { + "label": "singlepush", + "value": "4753" + }, + { + "label": "SushiKingB", + "value": "4745" + }, + { + "label": "Simon", + "value": "4743" + }, + { + "label": "St.LingYi", + "value": "4725" + }, + { + "label": "SisterLuo", + "value": "4724" + }, + { + "label": "saltedfish", + "value": "4723" + }, + { + "label": "SimpleXiaZ", + "value": "4706" + }, + { + "label": "SuMuQiuxue", + "value": "4702" + }, + { + "label": "sleepyanim", + "value": "4664" + }, + { + "label": "ScarletMoo", + "value": "4660" + }, + { + "label": "ShrimpBall", + "value": "4652" + }, + { + "label": "summerjoke", + "value": "4629" + }, + { + "label": "ssangrydra", + "value": "4625" + }, + { + "label": "ShowdownSa", + "value": "4613" + }, + { + "label": "second-han", + "value": "4577" + }, + { + "label": "Shame", + "value": "4565" + }, + { + "label": "ShuyinFloa", + "value": "4564" + }, + { + "label": "supportthe", + "value": "4554" + }, + { + "label": "Silverhair", + "value": "4540" + }, + { + "label": "SunflowerF", + "value": "4534" + }, + { + "label": "Skyshadowm", + "value": "4527" + }, + { + "label": "suddenlyit", + "value": "4517" + }, + { + "label": "spacetimes", + "value": "4508" + }, + { + "label": "stopfighti", + "value": "4507" + }, + { + "label": "ShanhaiSpe", + "value": "4473" + }, + { + "label": "Straightfo", + "value": "4470" + }, + { + "label": "SnowRoy", + "value": "4466" + }, + { + "label": "shesaiditw", + "value": "4465" + }, + { + "label": "successoro", + "value": "4454" + }, + { + "label": "sonnet", + "value": "4452" + }, + { + "label": "Sauerkraut", + "value": "4450" + }, + { + "label": "Siheyuanpi", + "value": "4448" + }, + { + "label": "Signboardm", + "value": "4438" + }, + { + "label": "stormyolds", + "value": "4434" + }, + { + "label": "ShiTianfen", + "value": "4427" + }, + { + "label": "sink", + "value": "4414" + }, + { + "label": "soslack", + "value": "4411" + }, + { + "label": "subjugatio", + "value": "4402" + }, + { + "label": "snowcherry", + "value": "4394" + }, + { + "label": "ShenShen", + "value": "4386" + }, + { + "label": "strawshoes", + "value": "4381" + }, + { + "label": "summer", + "value": "4372" + }, + { + "label": "spring", + "value": "4371" + }, + { + "label": "SaintPauli", + "value": "4367" + }, + { + "label": "Secondgive", + "value": "4366" + }, + { + "label": "skycity", + "value": "4352" + }, + { + "label": "Slvery", + "value": "4337" + }, + { + "label": "severekaro", + "value": "4314" + }, + { + "label": "stickfilia", + "value": "4308" + }, + { + "label": "Spike", + "value": "4304" + }, + { + "label": "snowinthec", + "value": "4295" + }, + { + "label": "Shura", + "value": "4287" + }, + { + "label": "StrideinCh", + "value": "4266" + }, + { + "label": "strongesta", + "value": "4255" + }, + { + "label": "SuYuyu", + "value": "4229" + }, + { + "label": "SkyFeather", + "value": "4226" + }, + { + "label": "Seelemywif", + "value": "4216" + }, + { + "label": "Seeyouatth", + "value": "4208" + }, + { + "label": "Solitary", + "value": "4198" + }, + { + "label": "spoilevery", + "value": "4177" + }, + { + "label": "sama", + "value": "4167" + }, + { + "label": "SongHuangl", + "value": "4165" + }, + { + "label": "Sijiu", + "value": "4162" + }, + { + "label": "skybird", + "value": "4112" + }, + { + "label": "smilecool", + "value": "4106" + }, + { + "label": "SecretFrag", + "value": "4105" + }, + { + "label": "SituQingfe", + "value": "4090" + }, + { + "label": "soarupbyda", + "value": "4075" + }, + { + "label": "Sakuracat", + "value": "4066" + }, + { + "label": "Sixi", + "value": "4055" + }, + { + "label": "SoIsay", + "value": "4043" + }, + { + "label": "so-and-soa", + "value": "4037" + }, + { + "label": "SkyMender", + "value": "4036" + }, + { + "label": "sellumbrel", + "value": "4035" + }, + { + "label": "sportsgeni", + "value": "4024" + }, + { + "label": "shadowfire", + "value": "4022" + }, + { + "label": "superdarkd", + "value": "4006" + }, + { + "label": "ShareHaiTs", + "value": "3996" + }, + { + "label": "smalltempl", + "value": "3990" + }, + { + "label": "saltyfox", + "value": "3986" + }, + { + "label": "sonoftheqi", + "value": "3968" + }, + { + "label": "SonofMyria", + "value": "3952" + }, + { + "label": "Stir-fried", + "value": "3911" + }, + { + "label": "stayupnigh", + "value": "3903" + }, + { + "label": "sunsetnear", + "value": "3901" + }, + { + "label": "slightlyco", + "value": "3890" + }, + { + "label": "sugarpoem", + "value": "3876" + }, + { + "label": "sixthousan", + "value": "3871" + }, + { + "label": "saltedfish", + "value": "3857" + }, + { + "label": "southfire", + "value": "3854" + }, + { + "label": "Softyander", + "value": "3840" + }, + { + "label": "slow-movin", + "value": "3825" + }, + { + "label": "Saygoodbye", + "value": "3822" + }, + { + "label": "Secondbatt", + "value": "3816" + }, + { + "label": "SkyStealin", + "value": "3805" + }, + { + "label": "Silentcare", + "value": "3787" + }, + { + "label": "strongbun", + "value": "3766" + }, + { + "label": "systemmale", + "value": "3734" + }, + { + "label": "Simmel", + "value": "3667" + }, + { + "label": "SmelltheFo", + "value": "3646" + }, + { + "label": "steamponk", + "value": "3559" + }, + { + "label": "Smart", + "value": "3557" + }, + { + "label": "SelfDiscip", + "value": "3548" + }, + { + "label": "Sect", + "value": "3547" + }, + { + "label": "starlessni", + "value": "3533" + }, + { + "label": "sifeizhai", + "value": "3531" + }, + { + "label": "ShreddedOn", + "value": "3529" + }, + { + "label": "silvercold", + "value": "3483" + }, + { + "label": "SoftSci-fi", + "value": "3425" + }, + { + "label": "Sugary", + "value": "3374" + }, + { + "label": "Senbeiboy", + "value": "3373" + }, + { + "label": "strongfema", + "value": "3062" + }, + { + "label": "strongfema", + "value": "3059" + }, + { + "label": "strongfema", + "value": "3057" + }, + { + "label": "strongfema", + "value": "3043" + }, + { + "label": "systemflow", + "value": "3040" + }, + { + "label": "Space-time", + "value": "3036" + }, + { + "label": "strongfema", + "value": "3033" + }, + { + "label": "systemflow", + "value": "3023" + }, + { + "label": "systemflow", + "value": "3015" + }, + { + "label": "Superman", + "value": "3011" + }, + { + "label": "Space-time", + "value": "2986" + }, + { + "label": "strongfema", + "value": "2985" + }, + { + "label": "systemflow", + "value": "2983" + }, + { + "label": "scumbagReb", + "value": "2981" + }, + { + "label": "systemflow", + "value": "2979" + }, + { + "label": "strongfema", + "value": "2958" + }, + { + "label": "strongfema", + "value": "2954" + }, + { + "label": "Shadowless", + "value": "2929" + }, + { + "label": "SonInLaw", + "value": "2918" + }, + { + "label": "Show-biz", + "value": "2888" + }, + { + "label": "Stand", + "value": "2884" + }, + { + "label": "singlesalt", + "value": "2865" + }, + { + "label": "Superfire", + "value": "2852" + }, + { + "label": "SuperGodNo", + "value": "2849" + }, + { + "label": "silver", + "value": "2807" + }, + { + "label": "shadowfall", + "value": "2800" + }, + { + "label": "stopatfirs", + "value": "2799" + }, + { + "label": "SouthKefei", + "value": "2769" + }, + { + "label": "streamerbl", + "value": "2762" + }, + { + "label": "sunsetover", + "value": "2755" + }, + { + "label": "softorange", + "value": "2750" + }, + { + "label": "SwingingDe", + "value": "2733" + }, + { + "label": "sopoor", + "value": "2732" + }, + { + "label": "supernovab", + "value": "2731" + }, + { + "label": "SacrificeX", + "value": "2727" + }, + { + "label": "speechless", + "value": "2692" + }, + { + "label": "Swimmingfi", + "value": "2686" + }, + { + "label": "SaltedFish", + "value": "2685" + }, + { + "label": "swearnotto", + "value": "2670" + }, + { + "label": "starfish", + "value": "2667" + }, + { + "label": "Sencha", + "value": "2664" + }, + { + "label": "Smokebambo", + "value": "2644" + }, + { + "label": "Sakurajima", + "value": "2639" + }, + { + "label": "swordrepai", + "value": "2617" + }, + { + "label": "summernow", + "value": "2574" + }, + { + "label": "ShenhaoMec", + "value": "2561" + }, + { + "label": "sistercook", + "value": "2553" + }, + { + "label": "Smallmushr", + "value": "2552" + }, + { + "label": "ShenJin", + "value": "2543" + }, + { + "label": "Straightme", + "value": "2528" + }, + { + "label": "stonemored", + "value": "2512" + }, + { + "label": "SongoftheG", + "value": "2509" + }, + { + "label": "stablefort", + "value": "2506" + }, + { + "label": "Stomachhur", + "value": "2493" + }, + { + "label": "Secretobse", + "value": "2483" + }, + { + "label": "StewedChic", + "value": "2482" + }, + { + "label": "sleeplesst", + "value": "2471" + }, + { + "label": "stevec", + "value": "2462" + }, + { + "label": "secondpira", + "value": "2428" + }, + { + "label": "soulanddre", + "value": "2424" + }, + { + "label": "SoulCelest", + "value": "2415" + }, + { + "label": "SanmitheGr", + "value": "2401" + }, + { + "label": "Supernatur", + "value": "2385" + }, + { + "label": "SuWei", + "value": "2383" + }, + { + "label": "Simpleone", + "value": "2382" + }, + { + "label": "sisterisbe", + "value": "2379" + }, + { + "label": "stardarkni", + "value": "2376" + }, + { + "label": "self-disci", + "value": "2372" + }, + { + "label": "signinsalt", + "value": "2371" + }, + { + "label": "SillyColum", + "value": "2363" + }, + { + "label": "森罗", + "value": "2361" + }, + { + "label": "specialwar", + "value": "2359" + }, + { + "label": "StudentUni", + "value": "2358" + }, + { + "label": "ShuYuChenX", + "value": "2346" + }, + { + "label": "StinkBeanS", + "value": "2343" + }, + { + "label": "Silencehim", + "value": "2341" + }, + { + "label": "SkinButler", + "value": "2322" + }, + { + "label": "scumteache", + "value": "2308" + }, + { + "label": "shadowghos", + "value": "2302" + }, + { + "label": "SaltedFish", + "value": "2299" + }, + { + "label": "SystemNo.3", + "value": "2281" + }, + { + "label": "SiheyuanDe", + "value": "2276" + }, + { + "label": "SuperPiran", + "value": "2273" + }, + { + "label": "Sweetandso", + "value": "2261" + }, + { + "label": "startofthe", + "value": "2258" + }, + { + "label": "SuZiyouyou", + "value": "2252" + }, + { + "label": "SiheyuanGo", + "value": "2246" + }, + { + "label": "SoulChef", + "value": "2244" + }, + { + "label": "Sword丨Lea", + "value": "2229" + }, + { + "label": "Sevengener", + "value": "2209" + }, + { + "label": "SakuraMoon", + "value": "2207" + }, + { + "label": "Siheyuanfl", + "value": "2175" + }, + { + "label": "sandrivere", + "value": "2116" + }, + { + "label": "sadsadness", + "value": "2086" + }, + { + "label": "ShenhuoxoR", + "value": "2074" + }, + { + "label": "silentkill", + "value": "2059" + }, + { + "label": "showstory", + "value": "2056" + }, + { + "label": "Sixty-six", + "value": "2052" + }, + { + "label": "snorkeling", + "value": "2038" + }, + { + "label": "stupidfox", + "value": "2020" + }, + { + "label": "SuperGodGr", + "value": "2014" + }, + { + "label": "sleepingsa", + "value": "2006" + }, + { + "label": "ShenLuo", + "value": "1993" + }, + { + "label": "Scourge", + "value": "1971" + }, + { + "label": "sleepslate", + "value": "1969" + }, + { + "label": "Sadreminde", + "value": "1936" + }, + { + "label": "six-twochi", + "value": "1913" + }, + { + "label": "sillycatse", + "value": "1909" + }, + { + "label": "SwordImmor", + "value": "1898" + }, + { + "label": "SuShaoqing", + "value": "1892" + }, + { + "label": "summertree", + "value": "1889" + }, + { + "label": "summertrip", + "value": "1888" + }, + { + "label": "sevenpigeo", + "value": "1883" + }, + { + "label": "SuYechen", + "value": "1878" + }, + { + "label": "仕辰", + "value": "1873" + }, + { + "label": "sundaysun", + "value": "1869" + }, + { + "label": "Shouldhand", + "value": "1860" + }, + { + "label": "Swordblood", + "value": "1852" + }, + { + "label": "SaltedFish", + "value": "1851" + }, + { + "label": "sadsword", + "value": "1840" + }, + { + "label": "sweetjelly", + "value": "1836" + }, + { + "label": "shudder", + "value": "1828" + }, + { + "label": "Scalesofth", + "value": "1825" + }, + { + "label": "shrimpinth", + "value": "1820" + }, + { + "label": "ServantofZ", + "value": "1817" + }, + { + "label": "Shallowsea", + "value": "1805" + }, + { + "label": "SmokeCloud", + "value": "1798" + }, + { + "label": "smokeinthe", + "value": "1790" + }, + { + "label": "Science", + "value": "1782" + }, + { + "label": "suicidalpr", + "value": "1774" + }, + { + "label": "singer", + "value": "1770" + }, + { + "label": "submissive", + "value": "1760" + }, + { + "label": "strongfema", + "value": "1747" + }, + { + "label": "schemeandc", + "value": "1742" + }, + { + "label": "Sailing", + "value": "1734" + }, + { + "label": "Siscon", + "value": "1726" + }, + { + "label": "shounenai", + "value": "1719" + }, + { + "label": "SchoolSett", + "value": "1678" + }, + { + "label": "Stepmother", + "value": "1626" + }, + { + "label": "SxFriends", + "value": "1581" + }, + { + "label": "SlapstickC", + "value": "1566" + }, + { + "label": "Saint", + "value": "1561" + }, + { + "label": "SpecialAbi", + "value": "1558" + }, + { + "label": "Supportive", + "value": "1524" + }, + { + "label": "Scientist", + "value": "1523" + }, + { + "label": "StrongestP", + "value": "1520" + }, + { + "label": "Shelter", + "value": "1506" + }, + { + "label": "Sysetm", + "value": "1501" + }, + { + "label": "sweetroman", + "value": "1488" + }, + { + "label": "SystemTran", + "value": "1468" + }, + { + "label": "Schemes", + "value": "1460" + }, + { + "label": "starwars", + "value": "1433" + }, + { + "label": "sport", + "value": "1432" + }, + { + "label": "secondchan", + "value": "1410" + }, + { + "label": "SaikiK.", + "value": "1382" + }, + { + "label": "SkillSteal", + "value": "1353" + }, + { + "label": "SwallowedS", + "value": "1339" + }, + { + "label": "SystemTran", + "value": "1330" + }, + { + "label": "SwordArtOn", + "value": "1320" + }, + { + "label": "SpiritualQ", + "value": "1307" + }, + { + "label": "SchemingPr", + "value": "1283" + }, + { + "label": "SlightlySu", + "value": "1270" + }, + { + "label": "School-lif", + "value": "1267" + }, + { + "label": "SpiritAnal", + "value": "1237" + }, + { + "label": "SpiritualR", + "value": "1219" + }, + { + "label": "StrongFema", + "value": "1200" + }, + { + "label": "Sequel", + "value": "1179" + }, + { + "label": "SameSexMar", + "value": "1170" + }, + { + "label": "SpecialLov", + "value": "1162" + }, + { + "label": "SonOfAGodP", + "value": "1132" + }, + { + "label": "Starcraft", + "value": "1120" + }, + { + "label": "SeaExplora", + "value": "1116" + }, + { + "label": "Si-fi", + "value": "1094" + }, + { + "label": "SaltedFish", + "value": "1089" + }, + { + "label": "StrongOpFe", + "value": "1065" + }, + { + "label": "slow-roman", + "value": "1011" + }, + { + "label": "Sci-Fantas", + "value": "1003" + }, + { + "label": "SoundMagic", + "value": "989" + }, + { + "label": "Skyrim", + "value": "979" + }, + { + "label": "SweetYaoi", + "value": "948" + }, + { + "label": "StrongPowe", + "value": "915" + }, + { + "label": "Streamer", + "value": "911" + }, + { + "label": "Strongfrom", + "value": "873" + }, + { + "label": "SystemAdmi", + "value": "865" + }, + { + "label": "Strongsubo", + "value": "862" + }, + { + "label": "SectMaster", + "value": "841" + }, + { + "label": "SuperSemin", + "value": "839" + }, + { + "label": "StrongSubo", + "value": "838" + }, + { + "label": "SlaveSyste", + "value": "828" + }, + { + "label": "Sentimenta", + "value": "771" + }, + { + "label": "SocialOutc", + "value": "755" + }, + { + "label": "Sibling&am", + "value": "750" + }, + { + "label": "SexualCult", + "value": "748" + }, + { + "label": "SlaveHarem", + "value": "730" + }, + { + "label": "SeeingThin", + "value": "633" + }, + { + "label": "SiblingsNo", + "value": "448" + }, + { + "label": "SchemesAnd", + "value": "405" + }, + { + "label": "Strength-b", + "value": "371" + }, + { + "label": "Student-Te", + "value": "365" + }, + { + "label": "Sharp-tong", + "value": "296" + }, + { + "label": "Transmigra", + "value": "32" + }, + { + "label": "trickery", + "value": "3157" + }, + { + "label": "TimeTravel", + "value": "199" + }, + { + "label": "two-waycru", + "value": "3115" + }, + { + "label": "ThreeKingd", + "value": "1356" + }, + { + "label": "technology", + "value": "3288" + }, + { + "label": "Troubledti", + "value": "3092" + }, + { + "label": "TangDynast", + "value": "3107" + }, + { + "label": "Technology", + "value": "1121" + }, + { + "label": "timegate", + "value": "3336" + }, + { + "label": "Trueandfal", + "value": "3189" + }, + { + "label": "Tragedy", + "value": "887" + }, + { + "label": "Taoistprie", + "value": "3113" + }, + { + "label": "Talents", + "value": "1314" + }, + { + "label": "Teacher", + "value": "664" + }, + { + "label": "Technologi", + "value": "488" + }, + { + "label": "TimeSkip", + "value": "410" + }, + { + "label": "two-wayred", + "value": "3321" + }, + { + "label": "thescienti", + "value": "3213" + }, + { + "label": "Terracenea", + "value": "3342" + }, + { + "label": "TheFourthC", + "value": "3210" + }, + { + "label": "Talentedgi", + "value": "3398" + }, + { + "label": "TragicPast", + "value": "140" + }, + { + "label": "Toughgirl", + "value": "3142" + }, + { + "label": "TradeWar", + "value": "3100" + }, + { + "label": "theInterne", + "value": "3287" + }, + { + "label": "Tsundere", + "value": "349" + }, + { + "label": "Thief", + "value": "3630" + }, + { + "label": "Topstream", + "value": "3260" + }, + { + "label": "Thriller", + "value": "476" + }, + { + "label": "Twins", + "value": "196" + }, + { + "label": "TianTingwe", + "value": "3383" + }, + { + "label": "Teamwork", + "value": "101" + }, + { + "label": "TimeManipu", + "value": "672" + }, + { + "label": "Twodimensi", + "value": "6144" + }, + { + "label": "Tianshi", + "value": "3709" + }, + { + "label": "takehome", + "value": "3434" + }, + { + "label": "Thieves", + "value": "420" + }, + { + "label": "timeandspa", + "value": "6173" + }, + { + "label": "Teachers", + "value": "252" + }, + { + "label": "TwistedPer", + "value": "499" + }, + { + "label": "TreasureCh", + "value": "3635" + }, + { + "label": "Thestronga", + "value": "5076" + }, + { + "label": "TimeLoop", + "value": "702" + }, + { + "label": "Transplant", + "value": "525" + }, + { + "label": "TribalSoci", + "value": "465" + }, + { + "label": "TS", + "value": "4334" + }, + { + "label": "transmigat", + "value": "1436" + }, + { + "label": "TomboyishF", + "value": "466" + }, + { + "label": "Theprideof", + "value": "6132" + }, + { + "label": "trackandfi", + "value": "3610" + }, + { + "label": "twilight", + "value": "1964" + }, + { + "label": "TopMC", + "value": "1655" + }, + { + "label": "Toriko", + "value": "1336" + }, + { + "label": "Threesome", + "value": "1042" + }, + { + "label": "TimeParado", + "value": "777" + }, + { + "label": "Torture", + "value": "728" + }, + { + "label": "Talent", + "value": "586" + }, + { + "label": "Two-dimens", + "value": "6138" + }, + { + "label": "travel", + "value": "4353" + }, + { + "label": "TsukibaAki", + "value": "2138" + }, + { + "label": "ThaiNovel", + "value": "1683" + }, + { + "label": "Transmigra", + "value": "1512" + }, + { + "label": "TerritoryC", + "value": "1329" + }, + { + "label": "Transmigra", + "value": "1098" + }, + { + "label": "Technology", + "value": "961" + }, + { + "label": "Traverse", + "value": "901" + }, + { + "label": "Tennis", + "value": "519" + }, + { + "label": "TimidProta", + "value": "463" + }, + { + "label": "Trap", + "value": "348" + }, + { + "label": "TeacherHua", + "value": "5866" + }, + { + "label": "Transforma", + "value": "5686" + }, + { + "label": "toast", + "value": "4801" + }, + { + "label": "TaihoZwei", + "value": "4676" + }, + { + "label": "Threeways", + "value": "4107" + }, + { + "label": "TombRaider", + "value": "3978" + }, + { + "label": "theseventh", + "value": "3473" + }, + { + "label": "Toilet", + "value": "2716" + }, + { + "label": "ThreshingG", + "value": "1950" + }, + { + "label": "Time-Trave", + "value": "1940" + }, + { + "label": "TangJichen", + "value": "1855" + }, + { + "label": "Tasker", + "value": "1628" + }, + { + "label": "TypeMoon", + "value": "1595" + }, + { + "label": "Tensura", + "value": "1594" + }, + { + "label": "Taoist", + "value": "1472" + }, + { + "label": "team", + "value": "1351" + }, + { + "label": "Transmigra", + "value": "1209" + }, + { + "label": "Trade", + "value": "863" + }, + { + "label": "TerminalIl", + "value": "658" + }, + { + "label": "Travelthro", + "value": "6134" + }, + { + "label": "Travelinga", + "value": "6115" + }, + { + "label": "Thejourney", + "value": "6104" + }, + { + "label": "Thecatwhos", + "value": "6093" + }, + { + "label": "Theprovinc", + "value": "6092" + }, + { + "label": "TianbangCh", + "value": "6087" + }, + { + "label": "Therainisf", + "value": "6077" + }, + { + "label": "Tooold", + "value": "6060" + }, + { + "label": "Tornadodes", + "value": "6057" + }, + { + "label": "TianbangOt", + "value": "6051" + }, + { + "label": "Threeyolks", + "value": "6044" + }, + { + "label": "Thewindblo", + "value": "6039" + }, + { + "label": "天律", + "value": "6032" + }, + { + "label": "thewholewo", + "value": "6021" + }, + { + "label": "Twohorsesa", + "value": "6010" + }, + { + "label": "Thereisabu", + "value": "6003" + }, + { + "label": "Threepiece", + "value": "5995" + }, + { + "label": "Timeissile", + "value": "5987" + }, + { + "label": "TaurenWarr", + "value": "5985" + }, + { + "label": "Thiswaterm", + "value": "5955" + }, + { + "label": "ThatMing", + "value": "5944" + }, + { + "label": "Turnonairc", + "value": "5943" + }, + { + "label": "TachibanaM", + "value": "5934" + }, + { + "label": "Thefishkin", + "value": "5920" + }, + { + "label": "TeacherDap", + "value": "5905" + }, + { + "label": "thirteenth", + "value": "5871" + }, + { + "label": "Theswordsw", + "value": "5869" + }, + { + "label": "theworldwi", + "value": "5838" + }, + { + "label": "Theplayofg", + "value": "5832" + }, + { + "label": "TCOCChrysa", + "value": "5807" + }, + { + "label": "tigerqueen", + "value": "5769" + }, + { + "label": "TheWizardS", + "value": "5752" + }, + { + "label": "Theoldboyw", + "value": "5735" + }, + { + "label": "Threehundr", + "value": "5717" + }, + { + "label": "TwoJinSuia", + "value": "5704" + }, + { + "label": "ThreeKingd", + "value": "5693" + }, + { + "label": "TheChaosOr", + "value": "5636" + }, + { + "label": "Therearewh", + "value": "5575" + }, + { + "label": "TheBeginni", + "value": "5574" + }, + { + "label": "tonightrai", + "value": "5567" + }, + { + "label": "talkingtig", + "value": "5562" + }, + { + "label": "Turnthecir", + "value": "5561" + }, + { + "label": "Theghostre", + "value": "5539" + }, + { + "label": "TianNaixin", + "value": "5504" + }, + { + "label": "TomatoUme", + "value": "5499" + }, + { + "label": "Thebloodco", + "value": "5492" + }, + { + "label": "Thekingask", + "value": "5488" + }, + { + "label": "theendofth", + "value": "5430" + }, + { + "label": "ThreeThous", + "value": "5419" + }, + { + "label": "TaibaiJun1", + "value": "5417" + }, + { + "label": "TwistedChe", + "value": "5385" + }, + { + "label": "tealcan&am", + "value": "5372" + }, + { + "label": "TroubledTi", + "value": "5349" + }, + { + "label": "trickerySl", + "value": "5335" + }, + { + "label": "thunderand", + "value": "5289" + }, + { + "label": "TsingYiLao", + "value": "5264" + }, + { + "label": "tentaclemu", + "value": "5249" + }, + { + "label": "Thereisnos", + "value": "5246" + }, + { + "label": "ThreeTower", + "value": "5239" + }, + { + "label": "Thefiveele", + "value": "5226" + }, + { + "label": "tigerroar", + "value": "5225" + }, + { + "label": "Thosewhowa", + "value": "5210" + }, + { + "label": "Tianbangbi", + "value": "5197" + }, + { + "label": "Thereisana", + "value": "5196" + }, + { + "label": "thousandso", + "value": "5163" + }, + { + "label": "threethous", + "value": "5151" + }, + { + "label": "Thirtycatt", + "value": "5142" + }, + { + "label": "TheDevil", + "value": "5075" + }, + { + "label": "TheMainCha", + "value": "5074" + }, + { + "label": "Terrori", + "value": "5068" + }, + { + "label": "teachermal", + "value": "5052" + }, + { + "label": "Thanksgivi", + "value": "5011" + }, + { + "label": "TianYixian", + "value": "5010" + }, + { + "label": "Tiggerlove", + "value": "4991" + }, + { + "label": "Therunaway", + "value": "4971" + }, + { + "label": "Tongsheng", + "value": "4926" + }, + { + "label": "Thetasteof", + "value": "4917" + }, + { + "label": "ThousandMo", + "value": "4899" + }, + { + "label": "TataTam", + "value": "4896" + }, + { + "label": "Theirstory", + "value": "4882" + }, + { + "label": "Thetwothor", + "value": "4851" + }, + { + "label": "thisisatru", + "value": "4845" + }, + { + "label": "Themorning", + "value": "4829" + }, + { + "label": "Thevoiceof", + "value": "4824" + }, + { + "label": "tallpoorha", + "value": "4789" + }, + { + "label": "threedayst", + "value": "4776" + }, + { + "label": "Thismanisn", + "value": "4759" + }, + { + "label": "turtlelike", + "value": "4750" + }, + { + "label": "thousandti", + "value": "4700" + }, + { + "label": "twotwoyell", + "value": "4648" + }, + { + "label": "ThousandSh", + "value": "4621" + }, + { + "label": "Theworldof", + "value": "4602" + }, + { + "label": "Thebluesta", + "value": "4592" + }, + { + "label": "threeclear", + "value": "4567" + }, + { + "label": "Theworldis", + "value": "4548" + }, + { + "label": "TheDailyLi", + "value": "4530" + }, + { + "label": "tomorrowwh", + "value": "4492" + }, + { + "label": "Tongzi", + "value": "4491" + }, + { + "label": "toothinpig", + "value": "4485" + }, + { + "label": "tigertiger", + "value": "4451" + }, + { + "label": "thefishint", + "value": "4445" + }, + { + "label": "TheAdventu", + "value": "4433" + }, + { + "label": "thirteen", + "value": "4405" + }, + { + "label": "Thebrillia", + "value": "4403" + }, + { + "label": "Tutuwantst", + "value": "4392" + }, + { + "label": "TangerineT", + "value": "4391" + }, + { + "label": "tomatofish", + "value": "4388" + }, + { + "label": "TheLonelyM", + "value": "4285" + }, + { + "label": "Thelong-te", + "value": "4181" + }, + { + "label": "thinkcaref", + "value": "4136" + }, + { + "label": "Transforme", + "value": "4123" + }, + { + "label": "Two-dimens", + "value": "4116" + }, + { + "label": "Thankyou", + "value": "4115" + }, + { + "label": "ten-cutmad", + "value": "4108" + }, + { + "label": "thejudge", + "value": "4102" + }, + { + "label": "threestick", + "value": "4092" + }, + { + "label": "theothersh", + "value": "4082" + }, + { + "label": "Throughthe", + "value": "4080" + }, + { + "label": "TianjiCour", + "value": "4067" + }, + { + "label": "Tiantong", + "value": "4054" + }, + { + "label": "touchyou", + "value": "4048" + }, + { + "label": "TianbangFa", + "value": "4041" + }, + { + "label": "TianrenCit", + "value": "4040" + }, + { + "label": "twilightmy", + "value": "4011" + }, + { + "label": "TianbangTo", + "value": "4004" + }, + { + "label": "TherealMr.", + "value": "3998" + }, + { + "label": "TigerSkinM", + "value": "3991" + }, + { + "label": "Thisconten", + "value": "3982" + }, + { + "label": "TheEndofWo", + "value": "3975" + }, + { + "label": "Tenconsecu", + "value": "3943" + }, + { + "label": "Turtle", + "value": "3937" + }, + { + "label": "TenYearsof", + "value": "3921" + }, + { + "label": "Taurus", + "value": "3905" + }, + { + "label": "Thefirstli", + "value": "3898" + }, + { + "label": "threethree", + "value": "3885" + }, + { + "label": "tenrabbits", + "value": "3878" + }, + { + "label": "Theoldmani", + "value": "3856" + }, + { + "label": "twelvemove", + "value": "3852" + }, + { + "label": "Timelimit", + "value": "3834" + }, + { + "label": "TheGreatDe", + "value": "3828" + }, + { + "label": "treefool", + "value": "3823" + }, + { + "label": "toiletlepr", + "value": "3794" + }, + { + "label": "threedaysf", + "value": "3786" + }, + { + "label": "Thelastday", + "value": "3741" + }, + { + "label": "teacher-st", + "value": "3731" + }, + { + "label": "ThugLuFeng", + "value": "3691" + }, + { + "label": "threeandfo", + "value": "3679" + }, + { + "label": "two", + "value": "3678" + }, + { + "label": "Thegodoffa", + "value": "3625" + }, + { + "label": "Themysteri", + "value": "3584" + }, + { + "label": "thedayisco", + "value": "3580" + }, + { + "label": "ThereisnoN", + "value": "3543" + }, + { + "label": "Twotwothre", + "value": "3536" + }, + { + "label": "tumbler", + "value": "3534" + }, + { + "label": "Theworldis", + "value": "3520" + }, + { + "label": "Thebestbra", + "value": "3514" + }, + { + "label": "TianyanShe", + "value": "3507" + }, + { + "label": "Thereisfir", + "value": "3500" + }, + { + "label": "TwoChildre", + "value": "3488" + }, + { + "label": "Theseahasn", + "value": "3486" + }, + { + "label": "theevening", + "value": "3469" + }, + { + "label": "Thegloryof", + "value": "3467" + }, + { + "label": "TianbangDi", + "value": "3371" + }, + { + "label": "Threerelig", + "value": "3360" + }, + { + "label": "Technology", + "value": "3348" + }, + { + "label": "trickery1V", + "value": "3075" + }, + { + "label": "Troubledti", + "value": "3054" + }, + { + "label": "Troubledti", + "value": "3052" + }, + { + "label": "trickeryin", + "value": "2970" + }, + { + "label": "TangDynast", + "value": "2941" + }, + { + "label": "ThreeKingd", + "value": "2935" + }, + { + "label": "TrueorFake", + "value": "2915" + }, + { + "label": "Thefishmar", + "value": "2871" + }, + { + "label": "TangThirty", + "value": "2843" + }, + { + "label": "Thetopofth", + "value": "2841" + }, + { + "label": "treeofenli", + "value": "2809" + }, + { + "label": "TeenageXia", + "value": "2784" + }, + { + "label": "twilightdr", + "value": "2775" + }, + { + "label": "Thisissure", + "value": "2768" + }, + { + "label": "twingods", + "value": "2764" + }, + { + "label": "Thewayofth", + "value": "2758" + }, + { + "label": "Thelistdep", + "value": "2747" + }, + { + "label": "Tianbanggr", + "value": "2741" + }, + { + "label": "TopoftheCl", + "value": "2740" + }, + { + "label": "Twopeopleb", + "value": "2714" + }, + { + "label": "ThreeDotIn", + "value": "2712" + }, + { + "label": "Today&0", + "value": "2699" + }, + { + "label": "Twopoundso", + "value": "2673" + }, + { + "label": "Thegloryof", + "value": "2671" + }, + { + "label": "towashthed", + "value": "2662" + }, + { + "label": "Twistbroth", + "value": "2645" + }, + { + "label": "takeoffboy", + "value": "2633" + }, + { + "label": "TenThousan", + "value": "2620" + }, + { + "label": "Two-dimens", + "value": "2612" + }, + { + "label": "TianbangHu", + "value": "2603" + }, + { + "label": "Thankyoufo", + "value": "2563" + }, + { + "label": "TimeKingJO", + "value": "2560" + }, + { + "label": "Thepowerof", + "value": "2537" + }, + { + "label": "TroubledWo", + "value": "2534" + }, + { + "label": "Tigerteeth", + "value": "2520" + }, + { + "label": "ThreeLives", + "value": "2503" + }, + { + "label": "Thousandso", + "value": "2498" + }, + { + "label": "Thecatisgo", + "value": "2475" + }, + { + "label": "Tsunderesc", + "value": "2464" + }, + { + "label": "Takeaplane", + "value": "2455" + }, + { + "label": "TrumanLive", + "value": "2452" + }, + { + "label": "TombRaider", + "value": "2444" + }, + { + "label": "Teemotofly", + "value": "2442" + }, + { + "label": "TombRaider", + "value": "2433" + }, + { + "label": "Two-dimens", + "value": "2427" + }, + { + "label": "takeoverth", + "value": "2421" + }, + { + "label": "TopoftheFo", + "value": "2420" + }, + { + "label": "Theashesar", + "value": "2419" + }, + { + "label": "ThousandTe", + "value": "2399" + }, + { + "label": "threeteeth", + "value": "2381" + }, + { + "label": "TwentyFame", + "value": "2380" + }, + { + "label": "Thelistisi", + "value": "2375" + }, + { + "label": "Two-dimens", + "value": "2373" + }, + { + "label": "Theoldfive", + "value": "2350" + }, + { + "label": "TombRaider", + "value": "2344" + }, + { + "label": "Tianshitak", + "value": "2338" + }, + { + "label": "Thirty-two", + "value": "2323" + }, + { + "label": "TianbangYa", + "value": "2311" + }, + { + "label": "TianYiding", + "value": "2306" + }, + { + "label": "Thetruegod", + "value": "2303" + }, + { + "label": "Thequeenis", + "value": "2300" + }, + { + "label": "Theancesto", + "value": "2259" + }, + { + "label": "Tianbangol", + "value": "2222" + }, + { + "label": "TangShaoqi", + "value": "2220" + }, + { + "label": "TombRaider", + "value": "2213" + }, + { + "label": "Theworld&a", + "value": "2205" + }, + { + "label": "threelittl", + "value": "2198" + }, + { + "label": "tobacco", + "value": "2187" + }, + { + "label": "Thinkingof", + "value": "2186" + }, + { + "label": "takestock", + "value": "2172" + }, + { + "label": "TenCommand", + "value": "2164" + }, + { + "label": "TheGodfath", + "value": "2161" + }, + { + "label": "Tianbangth", + "value": "2148" + }, + { + "label": "therearefi", + "value": "2143" + }, + { + "label": "Theoceando", + "value": "2115" + }, + { + "label": "TeckTyrann", + "value": "2107" + }, + { + "label": "Thestronge", + "value": "2105" + }, + { + "label": "TopoftheCl", + "value": "2103" + }, + { + "label": "铁帅", + "value": "2093" + }, + { + "label": "thewindisb", + "value": "2075" + }, + { + "label": "ThreeLives", + "value": "2072" + }, + { + "label": "Thenewbact", + "value": "2062" + }, + { + "label": "thisyear", + "value": "2034" + }, + { + "label": "Thebigdevi", + "value": "2024" + }, + { + "label": "TopoftheCl", + "value": "2023" + }, + { + "label": "Thebiggest", + "value": "2016" + }, + { + "label": "ToneMasaya", + "value": "2012" + }, + { + "label": "Thunderous", + "value": "1981" + }, + { + "label": "thegodofde", + "value": "1977" + }, + { + "label": "Technology", + "value": "1967" + }, + { + "label": "TempleThir", + "value": "1949" + }, + { + "label": "Tenthousan", + "value": "1939" + }, + { + "label": "TaurenIron", + "value": "1929" + }, + { + "label": "Thelightof", + "value": "1920" + }, + { + "label": "Three-flav", + "value": "1912" + }, + { + "label": "tomorrowwi", + "value": "1896" + }, + { + "label": "TingFengZh", + "value": "1884" + }, + { + "label": "TheThreeKi", + "value": "1872" + }, + { + "label": "Threedaysa", + "value": "1856" + }, + { + "label": "TheGospelo", + "value": "1854" + }, + { + "label": "Twilightis", + "value": "1850" + }, + { + "label": "Theflowero", + "value": "1808" + }, + { + "label": "Thesunsett", + "value": "1807" + }, + { + "label": "Tianbang78", + "value": "1785" + }, + { + "label": "Tyrant", + "value": "1762" + }, + { + "label": "TokyoGhoul", + "value": "1739" + }, + { + "label": "TerritoryM", + "value": "1725" + }, + { + "label": "TsundereLo", + "value": "1723" + }, + { + "label": "Tailsman", + "value": "1713" + }, + { + "label": "TheAsteris", + "value": "1672" + }, + { + "label": "TheGamer", + "value": "1668" + }, + { + "label": "traveller", + "value": "1625" + }, + { + "label": "Tramsmigra", + "value": "1601" + }, + { + "label": "Traveling", + "value": "1571" + }, + { + "label": "Trnasmigra", + "value": "1552" + }, + { + "label": "TalentShow", + "value": "1521" + }, + { + "label": "TimeandSpa", + "value": "1499" + }, + { + "label": "Tokyo", + "value": "1446" + }, + { + "label": "tv", + "value": "1431" + }, + { + "label": "theevernon", + "value": "1426" + }, + { + "label": "transmigra", + "value": "1420" + }, + { + "label": "TeacherMC", + "value": "1349" + }, + { + "label": "TeacherDis", + "value": "1348" + }, + { + "label": "TransportI", + "value": "1299" + }, + { + "label": "TreasureHu", + "value": "1238" + }, + { + "label": "Transportt", + "value": "1231" + }, + { + "label": "TheManInTh", + "value": "1225" + }, + { + "label": "TowerDefen", + "value": "1177" + }, + { + "label": "Twinbabies", + "value": "1176" + }, + { + "label": "Talismans", + "value": "1175" + }, + { + "label": "Transmigra", + "value": "1119" + }, + { + "label": "Transmigra", + "value": "1112" + }, + { + "label": "TravelingT", + "value": "1075" + }, + { + "label": "Transmigra", + "value": "1048" + }, + { + "label": "Tentacles", + "value": "1020" + }, + { + "label": "Transmigra", + "value": "968" + }, + { + "label": "TreasureHu", + "value": "964" + }, + { + "label": "TreasureHu", + "value": "942" + }, + { + "label": "TimeandSpa", + "value": "897" + }, + { + "label": "Tranformer", + "value": "852" + }, + { + "label": "Technology", + "value": "847" + }, + { + "label": "Tsuru", + "value": "801" + }, + { + "label": "Transporte", + "value": "790" + }, + { + "label": "Terrorists", + "value": "767" + }, + { + "label": "Titans", + "value": "753" + }, + { + "label": "Transporte", + "value": "554" + }, + { + "label": "TableTenni", + "value": "518" + }, + { + "label": "Transporte", + "value": "464" + }, + { + "label": "Trickster", + "value": "452" + }, + { + "label": "Transporte", + "value": "372" + }, + { + "label": "Transforma", + "value": "109" + }, + { + "label": "upgradeflo", + "value": "3204" + }, + { + "label": "UrbanLife", + "value": "893" + }, + { + "label": "upgrade", + "value": "3132" + }, + { + "label": "Urban", + "value": "350" + }, + { + "label": "UnlimitedF", + "value": "1530" + }, + { + "label": "urbanimmor", + "value": "3601" + }, + { + "label": "Uncle", + "value": "3593" + }, + { + "label": "Ugly", + "value": "3448" + }, + { + "label": "UglytoBeau", + "value": "483" + }, + { + "label": "UnluckyPro", + "value": "432" + }, + { + "label": "Unconditio", + "value": "141" + }, + { + "label": "Unrequited", + "value": "561" + }, + { + "label": "Urbanroman", + "value": "6157" + }, + { + "label": "Undercover", + "value": "3328" + }, + { + "label": "unknown", + "value": "2517" + }, + { + "label": "UniqueWeap", + "value": "735" + }, + { + "label": "Uglyface", + "value": "6040" + }, + { + "label": "Unprincipl", + "value": "5078" + }, + { + "label": "UniqueWeap", + "value": "751" + }, + { + "label": "UglyProtag", + "value": "739" + }, + { + "label": "Unreliable", + "value": "456" + }, + { + "label": "Undead", + "value": "4345" + }, + { + "label": "unyielding", + "value": "4098" + }, + { + "label": "UrbanRoman", + "value": "6187" + }, + { + "label": "Unremarkab", + "value": "5948" + }, + { + "label": "urbanyoung", + "value": "5907" + }, + { + "label": "Uncertainw", + "value": "5669" + }, + { + "label": "UltramanJu", + "value": "5658" + }, + { + "label": "Uchihaswor", + "value": "5578" + }, + { + "label": "UncleLiuHu", + "value": "5544" + }, + { + "label": "unclemerea", + "value": "5404" + }, + { + "label": "UncleArche", + "value": "5386" + }, + { + "label": "unfinished", + "value": "5381" + }, + { + "label": "UnderYeYuc", + "value": "5360" + }, + { + "label": "understood", + "value": "4843" + }, + { + "label": "Ultimatein", + "value": "4760" + }, + { + "label": "underthesn", + "value": "4616" + }, + { + "label": "Unexpected", + "value": "4574" + }, + { + "label": "UncleLiter", + "value": "4435" + }, + { + "label": "UncleMario", + "value": "4296" + }, + { + "label": "Universale", + "value": "4293" + }, + { + "label": "unlucky", + "value": "4119" + }, + { + "label": "Unlimiteds", + "value": "3955" + }, + { + "label": "Updatewith", + "value": "3951" + }, + { + "label": "Unique", + "value": "3845" + }, + { + "label": "underthest", + "value": "3743" + }, + { + "label": "underthera", + "value": "3537" + }, + { + "label": "UnrulyConf", + "value": "3472" + }, + { + "label": "Undefeated", + "value": "2850" + }, + { + "label": "Urbanyearn", + "value": "2846" + }, + { + "label": "Unlimitedc", + "value": "2844" + }, + { + "label": "understate", + "value": "2833" + }, + { + "label": "Unintentio", + "value": "2824" + }, + { + "label": "underlolic", + "value": "2702" + }, + { + "label": "UrbanMilit", + "value": "2505" + }, + { + "label": "undersilve", + "value": "2431" + }, + { + "label": "UnknownTao", + "value": "2422" + }, + { + "label": "urbanshark", + "value": "2256" + }, + { + "label": "Undead丨Kn", + "value": "2214" + }, + { + "label": "urbanstar", + "value": "2210" + }, + { + "label": "Upsetting", + "value": "2184" + }, + { + "label": "undeadfish", + "value": "2124" + }, + { + "label": "unbearable", + "value": "2048" + }, + { + "label": "UltramanPo", + "value": "1935" + }, + { + "label": "UrbanDatan", + "value": "1874" + }, + { + "label": "Unknowntea", + "value": "1833" + }, + { + "label": "Undocument", + "value": "1801" + }, + { + "label": "Urbanyouth", + "value": "1727" + }, + { + "label": "unexpected", + "value": "1395" + }, + { + "label": "Underestim", + "value": "580" + }, + { + "label": "UniqueCult", + "value": "323" + }, + { + "label": "Vest", + "value": "1504" + }, + { + "label": "Villain", + "value": "538" + }, + { + "label": "VirtualRea", + "value": "93" + }, + { + "label": "VinegarKin", + "value": "3195" + }, + { + "label": "Vampires", + "value": "16" + }, + { + "label": "Vanves", + "value": "3232" + }, + { + "label": "Variety", + "value": "3274" + }, + { + "label": "Vampire", + "value": "995" + }, + { + "label": "Villainess", + "value": "698" + }, + { + "label": "Videogame", + "value": "5080" + }, + { + "label": "Voice", + "value": "3224" + }, + { + "label": "VillainPro", + "value": "1637" + }, + { + "label": "videostrea", + "value": "3362" + }, + { + "label": "Villainess", + "value": "1618" + }, + { + "label": "VoiceActor", + "value": "549" + }, + { + "label": "VarietySho", + "value": "1645" + }, + { + "label": "VoicePack", + "value": "1319" + }, + { + "label": "violentthu", + "value": "5980" + }, + { + "label": "Victoria", + "value": "5819" + }, + { + "label": "VoidLinnos", + "value": "5463" + }, + { + "label": "veryobsess", + "value": "5402" + }, + { + "label": "Vicissitud", + "value": "5230" + }, + { + "label": "VillagerB", + "value": "5031" + }, + { + "label": "verypurean", + "value": "4957" + }, + { + "label": "Vodkaandmi", + "value": "4826" + }, + { + "label": "VoidStupid", + "value": "4623" + }, + { + "label": "videogames", + "value": "4339" + }, + { + "label": "Violence", + "value": "4338" + }, + { + "label": "VenerableP", + "value": "4210" + }, + { + "label": "Vientiane", + "value": "4183" + }, + { + "label": "VenusSoul", + "value": "4039" + }, + { + "label": "veryhappy", + "value": "3989" + }, + { + "label": "villageisv", + "value": "3935" + }, + { + "label": "verysleepy", + "value": "3933" + }, + { + "label": "VikaBaka", + "value": "3381" + }, + { + "label": "vampiredri", + "value": "2504" + }, + { + "label": "Viewofthec", + "value": "1973" + }, + { + "label": "Villains", + "value": "1548" + }, + { + "label": "VillIain", + "value": "1502" + }, + { + "label": "vampire", + "value": "1419" + }, + { + "label": "VillainEvi", + "value": "1323" + }, + { + "label": "VersatileM", + "value": "1222" + }, + { + "label": "VictorianE", + "value": "924" + }, + { + "label": "Vlogging", + "value": "912" + }, + { + "label": "WebNovel", + "value": "1673" + }, + { + "label": "WeaktoStro", + "value": "17" + }, + { + "label": "WearBook", + "value": "1154" + }, + { + "label": "Warm", + "value": "3216" + }, + { + "label": "WorldHoppi", + "value": "33" + }, + { + "label": "Wisdom", + "value": "3108" + }, + { + "label": "wealthyfam", + "value": "5046" + }, + { + "label": "WealthyCha", + "value": "7" + }, + { + "label": "WestwardJo", + "value": "3382" + }, + { + "label": "WorldTrave", + "value": "332" + }, + { + "label": "Wizards", + "value": "316" + }, + { + "label": "Whitemoonl", + "value": "3343" + }, + { + "label": "王者荣耀", + "value": "3393" + }, + { + "label": "workplaceb", + "value": "3571" + }, + { + "label": "Wastewoodf", + "value": "3442" + }, + { + "label": "Wars", + "value": "151" + }, + { + "label": "War", + "value": "848" + }, + { + "label": "WesternFan", + "value": "1387" + }, + { + "label": "wizardstre", + "value": "3301" + }, + { + "label": "White", + "value": "3453" + }, + { + "label": "Warmman", + "value": "3447" + }, + { + "label": "Workplace", + "value": "3271" + }, + { + "label": "Writers", + "value": "406" + }, + { + "label": "warrior", + "value": "3293" + }, + { + "label": "Writer", + "value": "872" + }, + { + "label": "WeakProtag", + "value": "477" + }, + { + "label": "Witches", + "value": "647" + }, + { + "label": "Whitecolla", + "value": "3748" + }, + { + "label": "wearback", + "value": "3147" + }, + { + "label": "Wizard", + "value": "902" + }, + { + "label": "World-hopp", + "value": "867" + }, + { + "label": "WaterMargi", + "value": "3355" + }, + { + "label": "Werebeasts", + "value": "765" + }, + { + "label": "whitelotus", + "value": "3749" + }, + { + "label": "Whitesnake", + "value": "3573" + }, + { + "label": "Wuxia", + "value": "626" + }, + { + "label": "wildlove", + "value": "5884" + }, + { + "label": "WorldofWar", + "value": "1703" + }, + { + "label": "Wishes", + "value": "562" + }, + { + "label": "Warhammer4", + "value": "796" + }, + { + "label": "windandclo", + "value": "3723" + }, + { + "label": "Wasteland", + "value": "898" + }, + { + "label": "WarsWeakto", + "value": "696" + }, + { + "label": "wastestrea", + "value": "6185" + }, + { + "label": "WangHuoyub", + "value": "5908" + }, + { + "label": "Werewolf", + "value": "5061" + }, + { + "label": "winter", + "value": "4150" + }, + { + "label": "Wholesome", + "value": "3839" + }, + { + "label": "Warlock", + "value": "3418" + }, + { + "label": "Westernrai", + "value": "2119" + }, + { + "label": "Wealth", + "value": "1536" + }, + { + "label": "WeektoStro", + "value": "1002" + }, + { + "label": "Wilderness", + "value": "884" + }, + { + "label": "WorldTree", + "value": "679" + }, + { + "label": "women&0", + "value": "6148" + }, + { + "label": "Weirdstori", + "value": "6135" + }, + { + "label": "wilderness", + "value": "6089" + }, + { + "label": "WolfTotem", + "value": "6058" + }, + { + "label": "windyblack", + "value": "6012" + }, + { + "label": "world", + "value": "5974" + }, + { + "label": "watertenta", + "value": "5967" + }, + { + "label": "wildradish", + "value": "5953" + }, + { + "label": "Wuxiabuysf", + "value": "5947" + }, + { + "label": "Whatareyou", + "value": "5926" + }, + { + "label": "Whitesparr", + "value": "5925" + }, + { + "label": "winterbird", + "value": "5876" + }, + { + "label": "Whattodoif", + "value": "5835" + }, + { + "label": "WriterUHae", + "value": "5833" + }, + { + "label": "王晏", + "value": "5816" + }, + { + "label": "windstopsn", + "value": "5801" + }, + { + "label": "汪叽", + "value": "5800" + }, + { + "label": "whatisyour", + "value": "5773" + }, + { + "label": "Wakeup", + "value": "5764" + }, + { + "label": "windy", + "value": "5751" + }, + { + "label": "往月", + "value": "5720" + }, + { + "label": "writebookq", + "value": "5718" + }, + { + "label": "WindWhispe", + "value": "5642" + }, + { + "label": "Writingfoo", + "value": "5615" + }, + { + "label": "woodenswor", + "value": "5599" + }, + { + "label": "Wastewoodn", + "value": "5594" + }, + { + "label": "Will-o&", + "value": "5550" + }, + { + "label": "wanttohold", + "value": "5540" + }, + { + "label": "Wannianfen", + "value": "5475" + }, + { + "label": "windandfor", + "value": "5459" + }, + { + "label": "weatapeach", + "value": "5369" + }, + { + "label": "wearbooksy", + "value": "5333" + }, + { + "label": "whitefox", + "value": "5300" + }, + { + "label": "wingedgras", + "value": "5293" + }, + { + "label": "WanTsang", + "value": "5270" + }, + { + "label": "WhiteDevil", + "value": "5266" + }, + { + "label": "wildcrumbs", + "value": "5263" + }, + { + "label": "温桑", + "value": "5192" + }, + { + "label": "what&03", + "value": "5181" + }, + { + "label": "witheredsh", + "value": "5169" + }, + { + "label": "Whattodowi", + "value": "5157" + }, + { + "label": "Writeabook", + "value": "5141" + }, + { + "label": "wildcatsus", + "value": "5140" + }, + { + "label": "Working", + "value": "5126" + }, + { + "label": "windup", + "value": "5110" + }, + { + "label": "WeaktoStro", + "value": "5097" + }, + { + "label": "Worlds", + "value": "5063" + }, + { + "label": "watermelon", + "value": "5007" + }, + { + "label": "whitelate", + "value": "5002" + }, + { + "label": "writebighe", + "value": "4977" + }, + { + "label": "Wanttoeatt", + "value": "4952" + }, + { + "label": "wittylittl", + "value": "4939" + }, + { + "label": "wipeandpic", + "value": "4911" + }, + { + "label": "whiterice", + "value": "4904" + }, + { + "label": "WenrenJing", + "value": "4880" + }, + { + "label": "WindMeteor", + "value": "4830" + }, + { + "label": "WriteBooks", + "value": "4820" + }, + { + "label": "winddance", + "value": "4783" + }, + { + "label": "whitegown", + "value": "4778" + }, + { + "label": "Wilde", + "value": "4777" + }, + { + "label": "WhenyourNP", + "value": "4748" + }, + { + "label": "WinterandS", + "value": "4744" + }, + { + "label": "worldofphi", + "value": "4737" + }, + { + "label": "WindandSno", + "value": "4728" + }, + { + "label": "warmalittl", + "value": "4722" + }, + { + "label": "watermelon", + "value": "4721" + }, + { + "label": "WhentheJun", + "value": "4707" + }, + { + "label": "whitefiref", + "value": "4701" + }, + { + "label": "walkonthee", + "value": "4658" + }, + { + "label": "Windandwea", + "value": "4632" + }, + { + "label": "weaklyacid", + "value": "4614" + }, + { + "label": "Winterfall", + "value": "4547" + }, + { + "label": "wanttolive", + "value": "4520" + }, + { + "label": "WangDachen", + "value": "4513" + }, + { + "label": "waterdropr", + "value": "4502" + }, + { + "label": "whiteconfe", + "value": "4501" + }, + { + "label": "warmmilkte", + "value": "4472" + }, + { + "label": "wealthy", + "value": "4467" + }, + { + "label": "What&03", + "value": "4430" + }, + { + "label": "Warcraft", + "value": "4360" + }, + { + "label": "wxya", + "value": "4310" + }, + { + "label": "Whenthedev", + "value": "4294" + }, + { + "label": "warmaster", + "value": "4251" + }, + { + "label": "wastebaske", + "value": "4247" + }, + { + "label": "WenYue", + "value": "4242" + }, + { + "label": "windchime~", + "value": "4176" + }, + { + "label": "woodbig", + "value": "4091" + }, + { + "label": "WuhuWuhu", + "value": "4073" + }, + { + "label": "wastedream", + "value": "4020" + }, + { + "label": "WongTingLi", + "value": "3983" + }, + { + "label": "windandwhe", + "value": "3930" + }, + { + "label": "watchtoget", + "value": "3926" + }, + { + "label": "Woodbrothe", + "value": "3916" + }, + { + "label": "warboss", + "value": "3904" + }, + { + "label": "wife-chasi", + "value": "3895" + }, + { + "label": "WeiyangXun", + "value": "3826" + }, + { + "label": "windtobebu", + "value": "3824" + }, + { + "label": "WanliWanhu", + "value": "3779" + }, + { + "label": "Witch", + "value": "3750" + }, + { + "label": "WW2", + "value": "3730" + }, + { + "label": "whitefoxfr", + "value": "3662" + }, + { + "label": "WithShehbu", + "value": "3521" + }, + { + "label": "watermolec", + "value": "3515" + }, + { + "label": "WangLin", + "value": "3504" + }, + { + "label": "waxypen", + "value": "3485" + }, + { + "label": "Willowleav", + "value": "3465" + }, + { + "label": "WarofCivil", + "value": "2906" + }, + { + "label": "WOW", + "value": "2894" + }, + { + "label": "wanttocome", + "value": "2859" + }, + { + "label": "whiteshirt", + "value": "2798" + }, + { + "label": "Whoringmak", + "value": "2713" + }, + { + "label": "windandmap", + "value": "2616" + }, + { + "label": "warmtime", + "value": "2606" + }, + { + "label": "watertown", + "value": "2585" + }, + { + "label": "WifeistheD", + "value": "2583" + }, + { + "label": "WenXuanyu", + "value": "2546" + }, + { + "label": "WenGuang", + "value": "2541" + }, + { + "label": "wanttoeatg", + "value": "2501" + }, + { + "label": "WangEr", + "value": "2459" + }, + { + "label": "wastefish", + "value": "2453" + }, + { + "label": "willowcand", + "value": "2447" + }, + { + "label": "Wuhutookof", + "value": "2429" + }, + { + "label": "witheredpr", + "value": "2413" + }, + { + "label": "WangXiaomi", + "value": "2353" + }, + { + "label": "Whitehorse", + "value": "2349" + }, + { + "label": "Walkinthec", + "value": "2297" + }, + { + "label": "Winningthe", + "value": "2294" + }, + { + "label": "whitekeybo", + "value": "2249" + }, + { + "label": "watermelon", + "value": "2169" + }, + { + "label": "WasteWoodA", + "value": "2167" + }, + { + "label": "witchfan", + "value": "2139" + }, + { + "label": "Wanderer", + "value": "2088" + }, + { + "label": "What&03", + "value": "2028" + }, + { + "label": "wanderings", + "value": "1963" + }, + { + "label": "WindSpirit", + "value": "1932" + }, + { + "label": "Wuxicheng", + "value": "1849" + }, + { + "label": "WangJiu", + "value": "1845" + }, + { + "label": "writeonlyz", + "value": "1837" + }, + { + "label": "wishardtow", + "value": "1819" + }, + { + "label": "wearingabo", + "value": "1718" + }, + { + "label": "Wearabook", + "value": "1612" + }, + { + "label": "WealthChar", + "value": "1602" + }, + { + "label": "WealthyCha", + "value": "1573" + }, + { + "label": "Warship", + "value": "1572" + }, + { + "label": "WorldWar2", + "value": "1495" + }, + { + "label": "war", + "value": "1408" + }, + { + "label": "WorldEmpir", + "value": "1337" + }, + { + "label": "WeaktoClan", + "value": "1325" + }, + { + "label": "WhiteBunSe", + "value": "1260" + }, + { + "label": "WarRecords", + "value": "1187" + }, + { + "label": "Werewolves", + "value": "1148" + }, + { + "label": "WebnovelSp", + "value": "1043" + }, + { + "label": "WorldHopin", + "value": "945" + }, + { + "label": "Warlocks", + "value": "792" + }, + { + "label": "Xiuxian", + "value": "3133" + }, + { + "label": "Xijing", + "value": "3387" + }, + { + "label": "Xianjun", + "value": "3173" + }, + { + "label": "Xiuwaihuih", + "value": "3353" + }, + { + "label": "Xianzun", + "value": "3446" + }, + { + "label": "Xianxia", + "value": "255" + }, + { + "label": "Xuanhuan", + "value": "539" + }, + { + "label": "XiuXiuXiuX", + "value": "1848" + }, + { + "label": "西幻", + "value": "6167" + }, + { + "label": "XianxiaCul", + "value": "6133" + }, + { + "label": "XiaoLinWei", + "value": "6119" + }, + { + "label": "Xiaoqianhu", + "value": "6081" + }, + { + "label": "Xiaomiisah", + "value": "6054" + }, + { + "label": "晓墨", + "value": "5939" + }, + { + "label": "XianYushan", + "value": "5874" + }, + { + "label": "Xizhilangc", + "value": "5781" + }, + { + "label": "XiaoJieJie", + "value": "5737" + }, + { + "label": "XuShiziqia", + "value": "5633" + }, + { + "label": "XL-423", + "value": "5546" + }, + { + "label": "Xiaoran", + "value": "5535" + }, + { + "label": "XiaoXiao", + "value": "5529" + }, + { + "label": "Xiaobuisno", + "value": "5439" + }, + { + "label": "Xinjia", + "value": "5380" + }, + { + "label": "Xiaoyuhast", + "value": "5224" + }, + { + "label": "Xiuxian&am", + "value": "5198" + }, + { + "label": "XiaJiBanxi", + "value": "4981" + }, + { + "label": "Xiaobaicry", + "value": "4973" + }, + { + "label": "XiaXiaolan", + "value": "4894" + }, + { + "label": "XiaTing", + "value": "4887" + }, + { + "label": "Xiaofei", + "value": "4747" + }, + { + "label": "XiaoMo&", + "value": "4742" + }, + { + "label": "XiaXiaXia", + "value": "4684" + }, + { + "label": "XiaoTangon", + "value": "4679" + }, + { + "label": "XiaoshanQi", + "value": "4669" + }, + { + "label": "Xiaohubuys", + "value": "4627" + }, + { + "label": "Xiaozhi", + "value": "4558" + }, + { + "label": "XuanyueQin", + "value": "4551" + }, + { + "label": "XueshenSan", + "value": "4521" + }, + { + "label": "XuShizi", + "value": "4515" + }, + { + "label": "XuYuanle", + "value": "4437" + }, + { + "label": "XiangziYan", + "value": "4379" + }, + { + "label": "XuanYuisea", + "value": "4279" + }, + { + "label": "Xier", + "value": "4257" + }, + { + "label": "XianmengXi", + "value": "4227" + }, + { + "label": "XiaJiEight", + "value": "4027" + }, + { + "label": "XNUMXDupda", + "value": "3985" + }, + { + "label": "Xuansheng", + "value": "3962" + }, + { + "label": "Xiawholove", + "value": "3912" + }, + { + "label": "Xiaothreey", + "value": "2858" + }, + { + "label": "XiaoxiangP", + "value": "2836" + }, + { + "label": "XiaomiStar", + "value": "2648" + }, + { + "label": "XiaonianXu", + "value": "2621" + }, + { + "label": "Xiaonianbl", + "value": "2529" + }, + { + "label": "XieDaoheng", + "value": "2519" + }, + { + "label": "XuIintheTa", + "value": "2477" + }, + { + "label": "Xuebaisinv", + "value": "2275" + }, + { + "label": "Xufamilyel", + "value": "2247" + }, + { + "label": "XuebaIII", + "value": "2231" + }, + { + "label": "小封", + "value": "2203" + }, + { + "label": "Xueqiunder", + "value": "2065" + }, + { + "label": "Xiaoxin", + "value": "1944" + }, + { + "label": "谢邀", + "value": "1904" + }, + { + "label": "Yandere", + "value": "510" + }, + { + "label": "Youth", + "value": "894" + }, + { + "label": "YuanandMin", + "value": "5696" + }, + { + "label": "Yancontrol", + "value": "3229" + }, + { + "label": "Yuri", + "value": "840" + }, + { + "label": "YoungerSis", + "value": "563" + }, + { + "label": "妖精", + "value": "3172" + }, + { + "label": "Yapi", + "value": "5683" + }, + { + "label": "Yaoi", + "value": "446" + }, + { + "label": "Yu-Gi-Oh!", + "value": "6202" + }, + { + "label": "YoungerLov", + "value": "769" + }, + { + "label": "YeYe", + "value": "2928" + }, + { + "label": "Yakumowhit", + "value": "5882" + }, + { + "label": "Yamasquirr", + "value": "5625" + }, + { + "label": "YawuZiyun", + "value": "5255" + }, + { + "label": "youzi", + "value": "5039" + }, + { + "label": "YuSu", + "value": "5015" + }, + { + "label": "YingTianMo", + "value": "4906" + }, + { + "label": "Yakumofami", + "value": "4121" + }, + { + "label": "YuYuyu", + "value": "2687" + }, + { + "label": "YeluChengj", + "value": "1829" + }, + { + "label": "Yugioh", + "value": "1263" + }, + { + "label": "YoungerBro", + "value": "747" + }, + { + "label": "yinandyang", + "value": "6143" + }, + { + "label": "Younghero", + "value": "6083" + }, + { + "label": "Yuanyuzhou", + "value": "6075" + }, + { + "label": "YunXiaoluo", + "value": "6073" + }, + { + "label": "Yahyfvs", + "value": "6043" + }, + { + "label": "YeTingfeng", + "value": "6025" + }, + { + "label": "夜独", + "value": "6009" + }, + { + "label": "Yunshan", + "value": "6008" + }, + { + "label": "Yibuchuckl", + "value": "5976" + }, + { + "label": "越泽", + "value": "5963" + }, + { + "label": "影", + "value": "5901" + }, + { + "label": "YouZhu", + "value": "5854" + }, + { + "label": "一条小段段", + "value": "5851" + }, + { + "label": "云怅", + "value": "5834" + }, + { + "label": "Youofthesk", + "value": "5732" + }, + { + "label": "YuanXiwu", + "value": "5652" + }, + { + "label": "YinGongziY", + "value": "5629" + }, + { + "label": "yandcrooke", + "value": "5627" + }, + { + "label": "Youarelike", + "value": "5612" + }, + { + "label": "YichenSwor", + "value": "5422" + }, + { + "label": "youareatra", + "value": "5356" + }, + { + "label": "Yutang", + "value": "5354" + }, + { + "label": "YanQiuyu", + "value": "5352" + }, + { + "label": "yoyo", + "value": "5299" + }, + { + "label": "YanyunYing", + "value": "5259" + }, + { + "label": "YoungMaste", + "value": "5234" + }, + { + "label": "丫大", + "value": "5214" + }, + { + "label": "yellowleav", + "value": "5158" + }, + { + "label": "youwholove", + "value": "5153" + }, + { + "label": "YouthLittl", + "value": "5152" + }, + { + "label": "Young", + "value": "5091" + }, + { + "label": "YunJiangdo", + "value": "4915" + }, + { + "label": "YeTong", + "value": "4846" + }, + { + "label": "YeFeiyu", + "value": "4762" + }, + { + "label": "yary", + "value": "4726" + }, + { + "label": "Yibai", + "value": "4716" + }, + { + "label": "YuShiyu", + "value": "4714" + }, + { + "label": "Yoooooooom", + "value": "4690" + }, + { + "label": "Youkaishan", + "value": "4672" + }, + { + "label": "YushuangYS", + "value": "4656" + }, + { + "label": "Yinglili&a", + "value": "4600" + }, + { + "label": "YuFengqian", + "value": "4550" + }, + { + "label": "Yourpupils", + "value": "4361" + }, + { + "label": "younglovei", + "value": "4326" + }, + { + "label": "YangXiaoli", + "value": "4311" + }, + { + "label": "YeChengzho", + "value": "4218" + }, + { + "label": "YaeSakurai", + "value": "4161" + }, + { + "label": "YeGuxue", + "value": "4151" + }, + { + "label": "Yueguhua", + "value": "4094" + }, + { + "label": "YuDaoan", + "value": "4084" + }, + { + "label": "Yuanyiunde", + "value": "3938" + }, + { + "label": "Yakumobloo", + "value": "3923" + }, + { + "label": "Yakumo", + "value": "3909" + }, + { + "label": "yonorth", + "value": "3881" + }, + { + "label": "YuMingYins", + "value": "3780" + }, + { + "label": "Yujielonga", + "value": "3773" + }, + { + "label": "Yunding丨P", + "value": "3583" + }, + { + "label": "YunZimo", + "value": "3544" + }, + { + "label": "Youalsowan", + "value": "3505" + }, + { + "label": "Yearningto", + "value": "2926" + }, + { + "label": "Yaoyue", + "value": "2864" + }, + { + "label": "YunZhongju", + "value": "2839" + }, + { + "label": "yearaftery", + "value": "2822" + }, + { + "label": "YeQianqiu", + "value": "2766" + }, + { + "label": "YoungMaste", + "value": "2757" + }, + { + "label": "yearningfo", + "value": "2743" + }, + { + "label": "YeGongzi", + "value": "2680" + }, + { + "label": "YoungMaste", + "value": "2669" + }, + { + "label": "YeGucheng", + "value": "2646" + }, + { + "label": "yearningfo", + "value": "2551" + }, + { + "label": "YuTsingYi", + "value": "2524" + }, + { + "label": "Yearningfo", + "value": "2365" + }, + { + "label": "YeXiaobai", + "value": "2286" + }, + { + "label": "YuboTiandi", + "value": "2282" + }, + { + "label": "Yakult", + "value": "2279" + }, + { + "label": "Ying&03", + "value": "2274" + }, + { + "label": "Yunmu", + "value": "2233" + }, + { + "label": "yearningfo", + "value": "2230" + }, + { + "label": "Yearningfo", + "value": "2193" + }, + { + "label": "YuXiaoqi", + "value": "2136" + }, + { + "label": "YangXiaoA", + "value": "2071" + }, + { + "label": "YingXiaofe", + "value": "2029" + }, + { + "label": "Yongchuang", + "value": "1983" + }, + { + "label": "YinLiisins", + "value": "1924" + }, + { + "label": "youaretoow", + "value": "1899" + }, + { + "label": "YellowSpri", + "value": "1788" + }, + { + "label": "Yu-Gi-Oh", + "value": "1085" + }, + { + "label": "直播", + "value": "3279" + }, + { + "label": "Zombies", + "value": "102" + }, + { + "label": "Zhutianliu", + "value": "6146" + }, + { + "label": "Zongmen", + "value": "3412" + }, + { + "label": "Zhenguan", + "value": "3391" + }, + { + "label": "Zergs", + "value": "1104" + }, + { + "label": "Zhengde", + "value": "3295" + }, + { + "label": "Zombie", + "value": "918" + }, + { + "label": "Zhangmenli", + "value": "6165" + }, + { + "label": "Zerg", + "value": "1076" + }, + { + "label": "ZhuZhiyue", + "value": "2123" + }, + { + "label": "ZhaoSisi", + "value": "4327" + }, + { + "label": "zeroclothe", + "value": "3827" + }, + { + "label": "Zippo", + "value": "2706" + }, + { + "label": "Zuge", + "value": "2150" + }, + { + "label": "ZiXuanXuan", + "value": "1903" + }, + { + "label": "z-man", + "value": "805" + }, + { + "label": "追妻", + "value": "6179" + }, + { + "label": "zerooverfl", + "value": "6005" + }, + { + "label": "Ziyingisno", + "value": "5927" + }, + { + "label": "醉猫", + "value": "5923" + }, + { + "label": "醉咏", + "value": "5895" + }, + { + "label": "ZuoJinghon", + "value": "5791" + }, + { + "label": "Zcraft", + "value": "5787" + }, + { + "label": "ZhouJijiu", + "value": "5611" + }, + { + "label": "ZhaoWenwub", + "value": "5498" + }, + { + "label": "ZhuqiuSanj", + "value": "5361" + }, + { + "label": "ZhaNiu", + "value": "5190" + }, + { + "label": "Zofi", + "value": "5005" + }, + { + "label": "ZhiLingInk", + "value": "4985" + }, + { + "label": "ZanpakutoK", + "value": "4968" + }, + { + "label": "ZhangLuo", + "value": "4703" + }, + { + "label": "Zhuge", + "value": "4563" + }, + { + "label": "Zhiyuan", + "value": "4477" + }, + { + "label": "Zhanginfro", + "value": "4276" + }, + { + "label": "Zanfran", + "value": "4246" + }, + { + "label": "ZhuangShis", + "value": "4234" + }, + { + "label": "ZiandIaren", + "value": "3811" + }, + { + "label": "ZhangGaoqi", + "value": "3756" + }, + { + "label": "Zerocharge", + "value": "3754" + }, + { + "label": "ZhangShuan", + "value": "3669" + }, + { + "label": "ZombieSumo", + "value": "3377" + }, + { + "label": "ZhuDabald", + "value": "2851" + }, + { + "label": "zhishen", + "value": "2697" + }, + { + "label": "ZuwuGonggo", + "value": "2676" + }, + { + "label": "ZhangErgou", + "value": "2653" + }, + { + "label": "Zhugeiscra", + "value": "2584" + }, + { + "label": "ZhugeDali&", + "value": "2576" + }, + { + "label": "ZhangFeiin", + "value": "2569" + }, + { + "label": "ZhangJuli", + "value": "2548" + }, + { + "label": "ZhangTianb", + "value": "2335" + }, + { + "label": "Zulongstil", + "value": "2266" + }, + { + "label": "zombiefish", + "value": "2257" + }, + { + "label": "ZhugeIrona", + "value": "2165" + }, + { + "label": "ZombieGod", + "value": "2145" + }, + { + "label": "ZombieQuee", + "value": "1577" + }, + { + "label": "zfighters", + "value": "1442" + }, + { + "label": "Zoo", + "value": "920" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/readwn/generator.js b/plugins/multisrc/readwn/generator.js new file mode 100644 index 000000000..c4ae9e9b6 --- /dev/null +++ b/plugins/multisrc/readwn/generator.js @@ -0,0 +1,40 @@ +import list from './sources.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); + +export const generateAll = function () { + return list.map(source => { + const exist = existsSync(join(folder, 'filters', source.id + '.json')); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.id + '.json'), + ); + source.filters = JSON.parse(filters).filters; + } + console.log( + `[readwn] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const readwnTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${readwnTemplate} +const plugin = new ReadwnPlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: 'english', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/readwn/get_filters.ts b/plugins/multisrc/readwn/get_filters.ts new file mode 100644 index 000000000..7c42c6a2b --- /dev/null +++ b/plugins/multisrc/readwn/get_filters.ts @@ -0,0 +1,155 @@ +require('module-alias/register'); +import * as fs from 'fs'; +import * as cheerio from 'cheerio'; +import * as path from 'path'; +import { Filters, FilterTypes, FilterOption } from '@libs/filterInputs'; +const type: string[] = ['genres', 'status', 'sort']; + +async function getFilters(name: string, url: string) { + const html = await fetch(url + '/list/all/all-newstime-0.html').then(res => + res.text(), + ); + const $: cheerio.CheerioAPI = cheerio.load(html); + const filters: Filters = { + 'sort': { + type: FilterTypes.Picker, + label: 'Sort By', + value: 'onclick', + options: [], + }, + 'status': { + type: FilterTypes.Picker, + label: 'Status', + value: 'all', + options: [], + }, + 'genres': { + type: FilterTypes.Picker, + label: 'Genre / Category', + value: '', + options: [], + }, + 'tags': { + type: FilterTypes.Picker, + label: 'Tags', + value: '', + options: [{ label: 'NONE', value: '' }], + }, + }; + + $('ul.proplist')?.each?.(function (index, ulElement) { + if (!type[index]) return; + $(ulElement) + .find('li > a') + .each((indx, liElement) => + filters[type[index]].options.push({ + label: $(liElement).text(), + value: $(liElement).attr('href'), + }), + ); + filters[type[index]].options = filters[type[index]].options.map( + (item: FilterOption) => { + let res = item.value; + if (index == 0) res = item.value.split('/')[2]; + if (index == 1) + res = item.value.replace(/\/list\/all\/(.*?)-.*$/, '$1'); + if (index == 2) res = item.value.replace(/.*\/all-(.*?)-.*/, '$1'); + return { + label: item.label, + value: res, + }; + }, + ) as FilterOption[]; + filters[type[index]].options.sort((a: FilterOption, b: FilterOption) => { + if (a.label === 'All') { + return -1; + } else if (b.label === 'All') { + return 1; + } + return a.label.localeCompare(b.label); + }); + }); + + const response = await fetch(url + '/browsetags/').then(res => res.text()); + const loadedCheerio: cheerio.CheerioAPI = cheerio.load(response); + const allPage = loadedCheerio('.tag-letters > a') + .map((index, element) => loadedCheerio(element).attr('href')) + .get(); + // ===================== tags ====================== + for (const page of allPage) { + console.log('fetch', url + page); + const resTags = await fetch(url + page).then(res => res.text()); + const $: cheerio.CheerioAPI = cheerio.load(resTags); + $('.tag-items > li > a').each((index, element) => + filters['tags'].options.push({ + label: $(element).text()?.trim(), + value: $(element) + .attr('href') + ?.split('/') + ?.pop() + ?.replace('-0.html', ''), + }), + ); + const nextPage = $('.pagination > li:last-child > a').attr('href'); + if (nextPage) { + const allpage = parseInt(nextPage.replace(/[^0-9]/g, '') || '0', 10); + for (let pageNo = 0; pageNo < allpage; pageNo++) { + await sleep(3000); + console.log( + 'fetch', + url + page.replace('-0.html', `-${pageNo + 1}.html`), + ); + const resTags = await fetch( + url + page.replace('-0.html', `-${pageNo + 1}.html`), + ).then(res => res.text()); + const $: cheerio.CheerioAPI = cheerio.load(resTags); + + $('.tag-items > li > a').each((index, element) => + filters['tags'].options.push({ + label: $(element).text()?.trim(), + value: $(element).attr('href')?.split('/')?.pop()?.split('-')?.[0], + }), + ); + } + } + await sleep(3000); + } + + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + JSON.stringify({ filters }, null, 2), + ); + console.log(`✅Filters created successfully for ${name}✅`); +} + +async function sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function askGetFilter() { + const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout, + }); + + const EREASE_PREV_LINE = '\x1b[1A\r\x1b[2K'; + await readline.question( + 'Enter the id of the site (same one as in sources.json): ', + async (name: string) => { + await readline.question( + EREASE_PREV_LINE + 'Enter the URL (same one as in sources.json): ', + async (url: string) => { + try { + await getFilters(name, url); + } catch (e: unknown) { + console.error('Error while getting filters from', url); + console.log(e instanceof Error ? e.message : e); + } + readline.close(); + }, + ); + }, + ); +} + +askGetFilter(); diff --git a/plugins/multisrc/readwn/settings.json b/plugins/multisrc/readwn/settings.json new file mode 100644 index 000000000..ef4480f9f --- /dev/null +++ b/plugins/multisrc/readwn/settings.json @@ -0,0 +1,193 @@ +{ + "filters": { + "sort": { + "label": "Sort By", + "options": [ + { + "label": "New", + "value": "newstime" + }, + { + "label": "Popular", + "value": "onclick" + }, + { + "label": "Updates", + "value": "lastdotime" + } + ], + "value": "newstime", + "type": "Picker" + }, + "status": { + "label": "Status", + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Completed", + "value": "Completed" + }, + { + "label": "Ongoing", + "value": "Ongoing" + } + ], + "value": "all", + "type": "Picker" + }, + "genres": { + "options": [ + { + "label": "All", + "value": "all" + }, + { + "label": "Action", + "value": "action" + }, + { + "label": "Adventure", + "value": "adventure" + }, + { + "label": "Comedy", + "value": "comedy" + }, + { + "label": "Contemporary Romance", + "value": "contemporary-romance" + }, + { + "label": "Drama", + "value": "drama" + }, + { + "label": "Eastern Fantasy", + "value": "eastern-fantasy" + }, + { + "label": "Fantasy", + "value": "fantasy" + }, + { + "label": "Fantasy Romance", + "value": "fantasy-romance" + }, + { + "label": "Gender Bender", + "value": "gender-bender" + }, + { + "label": "Harem", + "value": "harem" + }, + { + "label": "Historical", + "value": "historical" + }, + { + "label": "Horror", + "value": "horror" + }, + { + "label": "Josei", + "value": "josei" + }, + { + "label": "Lolicon", + "value": "lolicon" + }, + { + "label": "Magical Realism", + "value": "magical-realism" + }, + { + "label": "Martial Arts", + "value": "martial-arts" + }, + { + "label": "Mecha", + "value": "mecha" + }, + { + "label": "Mystery", + "value": "mystery" + }, + { + "label": "Psychological", + "value": "psychological" + }, + { + "label": "Romance", + "value": "romance" + }, + { + "label": "School Life", + "value": "school-life" + }, + { + "label": "Sci-fi", + "value": "sci-fi" + }, + { + "label": "Seinen", + "value": "seinen" + }, + { + "label": "Shoujo", + "value": "shoujo" + }, + { + "label": "Shounen", + "value": "shounen" + }, + { + "label": "Shounen Ai", + "value": "shounen-ai" + }, + { + "label": "Slice of Life", + "value": "slice-of-life" + }, + { + "label": "Sports", + "value": "sports" + }, + { + "label": "Supernatural", + "value": "supernatural" + }, + { + "label": "Tragedy", + "value": "tragedy" + }, + { + "label": "Video Games", + "value": "video-games" + }, + { + "label": "Wuxia", + "value": "wuxia" + }, + { + "label": "Xianxia", + "value": "xianxia" + }, + { + "label": "Xuanhuan", + "value": "xuanhuan" + }, + { + "label": "Yaoi", + "value": "yaoi" + } + ] + }, + "tags": { + "options": [{ "label": "Default", "value": "" }] + } + } +} diff --git a/plugins/multisrc/readwn/sources.json b/plugins/multisrc/readwn/sources.json new file mode 100644 index 000000000..70627905f --- /dev/null +++ b/plugins/multisrc/readwn/sources.json @@ -0,0 +1,57 @@ +[ + { + "id": "wuxiap", + "sourceSite": "https://www.wuxiabox.com", + "sourceName": "Wuxiabox" + }, + { + "id": "wuxiacity", + "sourceSite": "https://www.wuxiafox.com", + "sourceName": "Wuxiafox", + "options": { + "down": true, + "downSince": 1768289212969 + } + }, + { + "id": "ltnovel", + "sourceSite": "https://www.ltnovels.com", + "sourceName": "Ltnovel", + "options": { + "versionIncrements": 1 + } + }, + { + "id": "wuxiamtl", + "sourceSite": "https://www.fanmtl.com", + "sourceName": "Fans MTL", + "options": { + "versionIncrements": 2 + } + }, + { + "id": "fannovel", + "sourceSite": "https://www.fanmtl.com", + "sourceName": "FanNovel", + "options": { + "versionIncrements": 2 + } + }, + { + "id": "wuxiaspace", + "sourceSite": "https://www.wuxiaspot.com", + "sourceName": "Wuxia Space", + "options": { + "versionIncrements": 1 + } + }, + { + "id": "wuxiav", + "sourceSite": "https://www.wuxiav.com", + "sourceName": "WuxiaV", + "options": { + "down": true, + "downSince": 1768289212940 + } + } +] diff --git a/plugins/multisrc/readwn/template.ts b/plugins/multisrc/readwn/template.ts new file mode 100644 index 000000000..85c907fc2 --- /dev/null +++ b/plugins/multisrc/readwn/template.ts @@ -0,0 +1,230 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; + +type ReadwnOptions = { + versionIncrements?: number; +}; + +export type ReadwnMetadata = { + id: string; + sourceSite: string; + sourceName: string; + filters?: Filters; + options?: ReadwnOptions; +}; + +export class ReadwnPlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + filters?: Filters; + + constructor(metadata: ReadwnMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/readwn/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const versionIncrements = metadata.options?.versionIncrements || 0; + this.version = `1.0.${3 + versionIncrements}`; + this.filters = metadata.filters; + } + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/list/'; + url += (filters?.genres?.value || 'all') + '/'; + url += (filters?.status?.value || 'all') + '-'; + url += showLatestNovels ? 'lastdotime' : filters?.sort?.value || 'newstime'; + url += '-' + (pageNo - 1) + '.html'; + + if (filters?.tags?.value) { + //only 1 page + url = this.site + '/tags/' + filters.tags.value + '-0.html'; + } + + const body = await fetchApi(url).then((res: Response) => res.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') + .map((index, element) => ({ + name: loadedCheerio(element).find('h4').text() || '', + cover: + this.site + + loadedCheerio(element).find('.novel-cover > img').attr('data-src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then((res: Response) => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.novel-title').text() || '', + }; + + novel.author = loadedCheerio('span[itemprop=author]').text(); + novel.cover = + this.site + loadedCheerio('figure.cover > img').attr('data-src'); + + novel.summary = loadedCheerio('.summary') + .text() + .replace('Summary', '') + .trim(); + + novel.genres = loadedCheerio('div.categories > ul > li') + .map((index, element) => loadedCheerio(element).text()?.trim()) + .get() + .join(','); + + loadedCheerio('div.header-stats > span').each(function () { + if (loadedCheerio(this).find('small').text() === 'Status') { + novel.status = + loadedCheerio(this).find('strong').text() === 'Ongoing' + ? NovelStatus.Ongoing + : NovelStatus.Completed; + } + }); + + const latestChapterNo = parseInt( + loadedCheerio('.header-stats') + .find('span > strong') + .first() + .text() + .trim(), + ); + + const chapters: Plugin.ChapterItem[] = loadedCheerio('.chapter-list li') + .map((chapterIndex, element) => { + const name = loadedCheerio(element) + .find('a .chapter-title') + .text() + .trim(); + const path = loadedCheerio(element).find('a').attr('href')?.trim(); + if (!name || !path) return null; + + let releaseTime = loadedCheerio(element) + .find('a .chapter-update') + .text() + .trim(); + if (releaseTime?.includes?.('ago')) { + const timeAgo = releaseTime.match(/\d+/)?.[0] || '0'; + const timeAgoInt = parseInt(timeAgo, 10); + + if (timeAgoInt) { + const dayJSDate = dayjs(); // today + if ( + releaseTime.includes('hours ago') || + releaseTime.includes('hour ago') + ) { + dayJSDate.subtract(timeAgoInt, 'hours'); // go back N hours + } + + if ( + releaseTime.includes('days ago') || + releaseTime.includes('day ago') + ) { + dayJSDate.subtract(timeAgoInt, 'days'); // go back N days + } + + if ( + releaseTime.includes('months ago') || + releaseTime.includes('month ago') + ) { + dayJSDate.subtract(timeAgoInt, 'months'); // go back N months + } + + releaseTime = dayJSDate.format('LL'); + } + } + + return { + name, + path, + releaseTime, + chapterNumber: chapterIndex + 1, + }; + }) + .get() + .filter(chapter => chapter); + + if (latestChapterNo > chapters.length) { + const lastChapterNo = parseInt( + chapters[chapters.length - 1].path.match(/_(\d+)\.html/)?.[1] || '', + 10, + ); + + for ( + let i = (lastChapterNo || chapters.length) + 1; + i <= latestChapterNo; + i++ + ) { + chapters.push({ + name: 'Chapter ' + i, + path: novelPath.replace('.html', '_' + i + '.html'), + releaseTime: null, + chapterNumber: i, + }); + } + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then((res: Response) => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('.chapter-content').html() || ''; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const result = await fetchApi(this.site + '/e/search/index.php', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/search.html', + Origin: this.site, + }, + method: 'POST', + body: new URLSearchParams({ + show: 'title', + tempid: '1', + tbname: 'news', + keyboard: searchTerm, + }).toString(), + }).then((res: Response) => res.text()); + const loadedCheerio = parseHTML(result); + + const novels: Plugin.NovelItem[] = loadedCheerio('li.novel-item') + .map((index, element) => ({ + name: loadedCheerio(element).find('h4').text() || '', + cover: this.site + loadedCheerio(element).find('img').attr('data-src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + return novels; + } +} diff --git a/plugins/multisrc/rulate/filters/Bllate.json b/plugins/multisrc/rulate/filters/Bllate.json new file mode 100644 index 000000000..0cd4aada0 --- /dev/null +++ b/plugins/multisrc/rulate/filters/Bllate.json @@ -0,0 +1,179 @@ +{ + "filters": { + "genres": { + "type": "Checkbox", + "label": "Жанры: все жанры любой жанр", + "value": "", + "options": [ + { + "label": "арт", + "value": "1" + }, + { + "label": "боевик", + "value": "2" + }, + { + "label": "боевые искусства", + "value": "3" + }, + { + "label": "вампиры", + "value": "4" + }, + { + "label": "гарем", + "value": "5" + }, + { + "label": "гендерная интрига", + "value": "6" + }, + { + "label": "героическое фэнтези", + "value": "7" + }, + { + "label": "детектив", + "value": "8" + }, + { + "label": "дзёсэй", + "value": "9" + }, + { + "label": "додзинси", + "value": "10" + }, + { + "label": "драма", + "value": "11" + }, + { + "label": "игра", + "value": "12" + }, + { + "label": "история", + "value": "13" + }, + { + "label": "кодомо", + "value": "14" + }, + { + "label": "комедия", + "value": "15" + }, + { + "label": "махо-сёдзё", + "value": "16" + }, + { + "label": "меха", + "value": "17" + }, + { + "label": "мистика", + "value": "18" + }, + { + "label": "научная фантастика", + "value": "19" + }, + { + "label": "новый жанр", + "value": "45" + }, + { + "label": "повседневность", + "value": "20" + }, + { + "label": "постапокалиптика", + "value": "21" + }, + { + "label": "приключения", + "value": "22" + }, + { + "label": "психология", + "value": "23" + }, + { + "label": "романтика", + "value": "24" + }, + { + "label": "самурайский боевик", + "value": "25" + }, + { + "label": "сверхъестественное", + "value": "26" + }, + { + "label": "сёдзё", + "value": "27" + }, + { + "label": "сёдзё-ай", + "value": "28" + }, + { + "label": "сёнэн", + "value": "29" + }, + { + "label": "сёнэн-ай", + "value": "30" + }, + { + "label": "спорт", + "value": "31" + }, + { + "label": "сэйнэн", + "value": "32" + }, + { + "label": "сянься (XianXia)", + "value": "42" + }, + { + "label": "трагедия", + "value": "33" + }, + { + "label": "триллер", + "value": "34" + }, + { + "label": "ужасы", + "value": "35" + }, + { + "label": "уся (wuxia)", + "value": "41" + }, + { + "label": "фантастика", + "value": "36" + }, + { + "label": "фэнтези", + "value": "37" + }, + { + "label": "школа", + "value": "38" + }, + { + "label": "этти", + "value": "39" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/rulate/filters/Erolate.json b/plugins/multisrc/rulate/filters/Erolate.json new file mode 100644 index 000000000..e8b16614b --- /dev/null +++ b/plugins/multisrc/rulate/filters/Erolate.json @@ -0,0 +1,295 @@ +{ + "filters": { + "genres": { + "type": "Checkbox", + "label": "Жанры: все жанры любой жанр", + "value": "", + "options": [ + { + "label": "анал", + "value": "2" + }, + { + "label": "Арт", + "value": "70" + }, + { + "label": "бдсм", + "value": "3" + }, + { + "label": "Боевые искусства", + "value": "52" + }, + { + "label": "большая грудь", + "value": "5" + }, + { + "label": "большая попка", + "value": "6" + }, + { + "label": "большой член", + "value": "7" + }, + { + "label": "бондаж", + "value": "8" + }, + { + "label": "в первый раз", + "value": "9" + }, + { + "label": "в цвете", + "value": "10" + }, + { + "label": "гарем", + "value": "11" + }, + { + "label": "Гарем", + "value": "71" + }, + { + "label": "гендарная интрига", + "value": "12" + }, + { + "label": "Героическое фэнтези", + "value": "57" + }, + { + "label": "групповой секс", + "value": "13" + }, + { + "label": "детектив", + "value": "39" + }, + { + "label": "Детектив", + "value": "50" + }, + { + "label": "Дзёсэй", + "value": "45" + }, + { + "label": "драма", + "value": "14" + }, + { + "label": "Драма", + "value": "46" + }, + { + "label": "зрелые женщины (milf)", + "value": "15" + }, + { + "label": "измена", + "value": "16" + }, + { + "label": "изнасилование", + "value": "17" + }, + { + "label": "инцест", + "value": "18" + }, + { + "label": "исторический", + "value": "19" + }, + { + "label": "История", + "value": "62" + }, + { + "label": "комедия", + "value": "20" + }, + { + "label": "Комедия", + "value": "51" + }, + { + "label": "контроль над разумом", + "value": "35" + }, + { + "label": "маленькая грудь", + "value": "21" + }, + { + "label": "мистика", + "value": "40" + }, + { + "label": "Мистика", + "value": "66" + }, + { + "label": "Мурим", + "value": "54" + }, + { + "label": "научная фантастика", + "value": "22" + }, + { + "label": "Научная фантастика", + "value": "72" + }, + { + "label": "нетораре", + "value": "23" + }, + { + "label": "оральный секс", + "value": "24" + }, + { + "label": "повседневность", + "value": "41" + }, + { + "label": "Повседневность", + "value": "53" + }, + { + "label": "Постапокалиптика", + "value": "55" + }, + { + "label": "приключения", + "value": "38" + }, + { + "label": "Приключения", + "value": "42" + }, + { + "label": "Психология", + "value": "63" + }, + { + "label": "публичный секс", + "value": "33" + }, + { + "label": "романтика", + "value": "25" + }, + { + "label": "Романтика", + "value": "47" + }, + { + "label": "с изображениями ", + "value": "36" + }, + { + "label": "сверхъестественное", + "value": "34" + }, + { + "label": "Сверхъестественное", + "value": "58" + }, + { + "label": "Сёдзё", + "value": "48" + }, + { + "label": "Сёнэн", + "value": "43" + }, + { + "label": "смат", + "value": "37" + }, + { + "label": "Спорт", + "value": "65" + }, + { + "label": "Сэйнэн", + "value": "64" + }, + { + "label": "тентакли", + "value": "26" + }, + { + "label": "трагедия", + "value": "27" + }, + { + "label": "Трагедия", + "value": "61" + }, + { + "label": "Триллер", + "value": "69" + }, + { + "label": "ужасы", + "value": "28" + }, + { + "label": "Ужасы", + "value": "68" + }, + { + "label": "фантастика", + "value": "32" + }, + { + "label": "Фантастика", + "value": "60" + }, + { + "label": "фэнтези", + "value": "29" + }, + { + "label": "Фэнтези", + "value": "44" + }, + { + "label": "чикан", + "value": "30" + }, + { + "label": "Школьная жизнь", + "value": "59" + }, + { + "label": "Экшен", + "value": "49" + }, + { + "label": "Эротика", + "value": "56" + }, + { + "label": "этти", + "value": "31" + }, + { + "label": "Этти", + "value": "67" + }, + { + "label": "ahegao", + "value": "1" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/rulate/filters/Rulate.json b/plugins/multisrc/rulate/filters/Rulate.json new file mode 100644 index 000000000..57dfa2d72 --- /dev/null +++ b/plugins/multisrc/rulate/filters/Rulate.json @@ -0,0 +1,195 @@ +{ + "filters": { + "genres": { + "type": "Checkbox", + "label": "Жанры: все жанры любой жанр", + "value": "", + "options": [ + { + "label": "18+", + "value": "52" + }, + { + "label": "21+", + "value": "59" + }, + { + "label": "арт", + "value": "1" + }, + { + "label": "боевик", + "value": "2" + }, + { + "label": "боевые искусства", + "value": "3" + }, + { + "label": "гаремник", + "value": "5" + }, + { + "label": "героическое фэнтези", + "value": "7" + }, + { + "label": "городское фэнтези", + "value": "57" + }, + { + "label": "детектив", + "value": "8" + }, + { + "label": "дзёсэй", + "value": "9" + }, + { + "label": "додзинси", + "value": "10" + }, + { + "label": "драма", + "value": "11" + }, + { + "label": "история", + "value": "13" + }, + { + "label": "киберпанк", + "value": "46" + }, + { + "label": "кодомо", + "value": "14" + }, + { + "label": "комедия", + "value": "15" + }, + { + "label": "литрпг", + "value": "48" + }, + { + "label": "махо-сёдзё", + "value": "16" + }, + { + "label": "мелодрама", + "value": "49" + }, + { + "label": "меха", + "value": "17" + }, + { + "label": "мистика", + "value": "18" + }, + { + "label": "научная фантастика", + "value": "19" + }, + { + "label": "повседневность", + "value": "20" + }, + { + "label": "постапокалиптика", + "value": "21" + }, + { + "label": "приключения", + "value": "22" + }, + { + "label": "психология", + "value": "23" + }, + { + "label": "романтика", + "value": "24" + }, + { + "label": "самурайский боевик", + "value": "25" + }, + { + "label": "сверхъестественное", + "value": "26" + }, + { + "label": "сёдзё", + "value": "27" + }, + { + "label": "сёнэн", + "value": "29" + }, + { + "label": "смат", + "value": "45" + }, + { + "label": "спорт", + "value": "31" + }, + { + "label": "сэйнэн", + "value": "32" + }, + { + "label": "сюаньхуа", + "value": "44" + }, + { + "label": "сюаньхуань", + "value": "47" + }, + { + "label": "сянься (XianXia)", + "value": "42" + }, + { + "label": "трагедия", + "value": "33" + }, + { + "label": "триллер", + "value": "34" + }, + { + "label": "ужасы", + "value": "35" + }, + { + "label": "уся (wuxia)", + "value": "41" + }, + { + "label": "фантастика", + "value": "36" + }, + { + "label": "фанфик", + "value": "50" + }, + { + "label": "фэнтези", + "value": "37" + }, + { + "label": "школа", + "value": "38" + }, + { + "label": "этти", + "value": "39" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/multisrc/rulate/generator.js b/plugins/multisrc/rulate/generator.js new file mode 100644 index 000000000..bfda48f34 --- /dev/null +++ b/plugins/multisrc/rulate/generator.js @@ -0,0 +1,49 @@ +import list from './sources.json' with { type: 'json' }; +import defaultSettings from './settings.json' with { type: 'json' }; +import { existsSync, readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const folder = dirname(fileURLToPath(import.meta.url)); +const key = 'fpoiKLUues81werht039'; + +export const generateAll = function () { + return list.map(source => { + source.key = key; + source.filters = defaultSettings.filters; + + const exist = existsSync( + join(folder, 'filters', source.sourceName + '.json'), + ); + if (exist) { + const filters = readFileSync( + join(folder, 'filters', source.sourceName + '.json'), + ); + source.filters = Object.assign( + defaultSettings.filters, + JSON.parse(filters).filters, + ); + } + + console.log(`[rulate]: Generating`, source.id); + return generator(source); + }); +}; + +const generator = function generator(source) { + const rulateTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` + ${rulateTemplate} +const plugin = new RulatePlugin(${JSON.stringify(source)}); +export default plugin; + `.trim(); + + return { + lang: 'russian', + filename: source.sourceName, + pluginScript, + }; +}; diff --git a/plugins/multisrc/rulate/settings.json b/plugins/multisrc/rulate/settings.json new file mode 100644 index 000000000..6ef78b31c --- /dev/null +++ b/plugins/multisrc/rulate/settings.json @@ -0,0 +1,71 @@ +{ + "filters": { + "sort": { + "label": "Сортировка:", + "value": "6", + "options": [ + { + "label": "По рейтингу", + "value": "6" + }, + { + "label": "По дате последней активности", + "value": "4" + }, + { + "label": "По дате создания", + "value": "3" + }, + { + "label": "По кол-ву бесплатных глав", + "value": "11" + }, + { + "label": "По кол-ву в закладках", + "value": "13" + }, + { + "label": "По кол-ву в избранном", + "value": "14" + }, + { + "label": "По кол-ву лайков", + "value": "8" + }, + { + "label": "По кол-ву переведённых глав", + "value": "7" + }, + { + "label": "По кол-ву рецензий", + "value": "12" + }, + { + "label": "По кол-ву страниц", + "value": "10" + }, + { + "label": "По названию на языке оригинала", + "value": "1" + }, + { + "label": "По названию на языке перевода", + "value": "2" + }, + { + "label": "По просмотрам", + "value": "5" + }, + { + "label": "По степени готовности", + "value": "0" + }, + { + "label": "Случайно", + "value": "9" + } + ], + "type": "Picker" + } + } +} diff --git a/plugins/multisrc/rulate/sources.json b/plugins/multisrc/rulate/sources.json new file mode 100644 index 000000000..b149c5878 --- /dev/null +++ b/plugins/multisrc/rulate/sources.json @@ -0,0 +1,20 @@ +[ + { + "id": "rulate-api", + "sourceSite": "https://tl.rulate.ru", + "sourceName": "Rulate", + "versionIncrements": 0 + }, + { + "id": "erolate-api", + "sourceSite": "https://erolate.com", + "sourceName": "Erolate", + "versionIncrements": 0 + }, + { + "id": "bllate-api", + "sourceSite": "https://bllate.org", + "sourceName": "Bllate", + "versionIncrements": 0 + } +] diff --git a/plugins/multisrc/rulate/template.ts b/plugins/multisrc/rulate/template.ts new file mode 100644 index 000000000..a0c3383c3 --- /dev/null +++ b/plugins/multisrc/rulate/template.ts @@ -0,0 +1,210 @@ +import { fetchApi } from '@libs/fetch'; +import { FilterToValues, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +export type RulateMetadata = { + id: string; + sourceSite: string; + sourceName: string; + filters?: Filters; + versionIncrements: number; + key: string; +}; + +const headers = { + 'User-Agent': 'RuLateApp Android', + 'accept-encoding': 'gzip', +}; + +export class RulatePlugin implements Plugin.PluginBase { + id: string; + name: string; + icon: string; + site: string; + version: string; + filters?: Filters | undefined; + key: string; + + constructor(metadata: RulateMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName + ' (API)'; + this.icon = `multisrc/rulate/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + this.version = '1.0.' + (1 + metadata.versionIncrements); + this.filters = metadata.filters; + this.key = metadata.key; + } + + parseNovels(url: string) { + return fetchApi(url, { headers }) + .then((res: Response) => res.json() as Promise<SearchResponse>) + .then((data: SearchResponse) => { + const novels: Plugin.NovelItem[] = []; + + if (data.status === 'success' && data.response?.length) { + data.response.forEach(novel => + novels.push({ + name: novel.t_title || novel.s_title, + path: novel.id.toString(), + cover: novel.img, + }), + ); + } + + return novels; + }); + } + + async popularNovels( + page: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/api3/searchBooks?limit=40&page=' + page; + url += '&sort=' + (showLatestNovels ? '4' : filters?.sort?.value || '6'); + + Object.entries(filters || {}).forEach(([type, filter]) => { + const { value } = filter as FilterToValues<Filters>[string]; + if (value instanceof Array && value.length) { + url += '&' + value.map(val => type + '[]=' + val).join('&'); + } + }); + + url += '&key=' + this.key; + return this.parseNovels(url); + } + + async searchNovels( + searchTerm: string, + page = 1, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/api3/searchBooks?t=${encodeURIComponent( + searchTerm, + )}&limit=40&page=${page}&key=${this.key}`; + return this.parseNovels(url); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const book = await fetchApi( + this.site + '/api3/book?book_id=' + novelPath + '&key=' + this.key, + { headers }, + ).then((res: Response) => res.json() as Promise<BookResponse>); + + const novel: Plugin.SourceNovel = { + name: book.response.t_title || book.response.s_title, + path: novelPath, + cover: book.response.img, + genres: [book.response.genres, book.response.tags] + .flatMap(c => + c?.map?.((g: { title?: string; name?: string }) => g.title || g.name), + ) + .join(','), + summary: book.response.description, + author: book.response.author, + status: + book.response.status === 'Завершён' + ? NovelStatus.Completed + : NovelStatus.Ongoing, + rating: + book.response.rate && book.response.rate.count > 0 + ? Number( + (book.response.rate.sum / book.response.rate.count).toFixed(2), + ) + : undefined, + }; + + const chaptersData = await fetchApi( + this.site + + '/api3/bookChapters?book_id=' + + novelPath + + '&key=' + + this.key, + { headers }, + ).then((res: Response) => res.json() as Promise<ChaptersResponse>); + + const chapters: Plugin.ChapterItem[] = []; + + if (chaptersData.response && Array.isArray(chaptersData.response)) { + chaptersData.response.forEach((chapter: ChapterResponse) => { + if (chapter.can_read && chapter.subscription === 0) { + chapters.push({ + name: chapter.title + (chapter.illustrated ? ' 🖼️' : ''), + path: novelPath + '/' + chapter.id, + releaseTime: dayjs(chapter.cdate * 1000).format('LLL'), + chapterNumber: chapter.ord, + }); + } + }); + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [book, chapter] = chapterPath.split('/'); + const body = await fetchApi( + this.site + + '/api3/chapter?book_id=' + + book + + '&chapter_id=' + + chapter + + '&key=' + + this.key, + { headers }, + ).then((res: Response) => res.json() as Promise<ChapterTextResponse>); + + return body.response.text; + } + resolveUrl = (path: string, isNovel?: boolean) => + this.site + '/book/' + path + (isNovel ? '/' : '/ready_new'); +} + +type SearchResponse = { + status: string; + response: { + t_title?: string; + s_title: string; + id: number; + img: string; + }[]; +}; + +type BookResponse = { + response: { + t_title?: string; + s_title: string; + img: string; + cat?: { title: string }[]; + genres?: { title: string }[]; + tags?: { name: string }[]; + description: string; + author: string; + status: string; + rate?: { sum: number; count: number }; + }; +}; + +type ChapterResponse = { + title: string; + id: number; + ord: number; + cdate: number; + subscription: number; + can_read: boolean; + illustrated?: boolean; +}; + +type ChaptersResponse = { + response: ChapterResponse[]; +}; + +type ChapterTextResponse = { + response: { + text: string; + }; +}; diff --git a/plugins/multisrc/rulate/update_filters.js b/plugins/multisrc/rulate/update_filters.js new file mode 100644 index 000000000..0e2b978dd --- /dev/null +++ b/plugins/multisrc/rulate/update_filters.js @@ -0,0 +1,65 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const key = 'fpoiKLUues81werht039'; + +async function getFilters(name, url) { + const filters = { + genres: { + type: 'Checkbox', + label: 'Жанры: все жанры любой жанр', + value: '', + options: [], + }, + }; + + try { + const genres = await fetch(`${url}/api3/genres?key=${key}`).then(res => + res.json(), + ); + + if (genres.status === 'success' && genres.response?.length) { + genres.response.forEach(genre => { + filters.genres.options.push({ + label: genre.title, + value: genre.id.toString(), + }); + }); + filters.genres.options.sort((a, b) => a.label.localeCompare(b.label)); + } + + const filtersDir = path.join(__dirname, 'filters'); + if (!fs.existsSync(filtersDir)) { + fs.mkdirSync(filtersDir); + } + + fs.writeFileSync( + path.join(filtersDir, `${name}.json`), + JSON.stringify({ filters }, null, 2), + ); + + console.log(`✅ Filter updated for ${name}`); + } catch (error) { + console.error(`❌ Error processing filters for ${name}:`, error); + } +} + +try { + const sourcesRaw = fs.readFileSync( + path.join(__dirname, 'sources.json'), + 'utf-8', + ); + const sources = JSON.parse(sourcesRaw); + + for (const source of sources) { + if (source.id && source.sourceSite) { + await getFilters(source.sourceName, source.sourceSite); + } + } +} catch (e) { + console.error('Error reading or parsing sources.json', e); +} diff --git a/plugins/polish/novelki.ts b/plugins/polish/novelki.ts new file mode 100644 index 000000000..be3e1aff8 --- /dev/null +++ b/plugins/polish/novelki.ts @@ -0,0 +1,249 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { load as parseHTML } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; + +class NovelkiPL implements Plugin.PluginBase { + id = 'novelki.pl'; + name = 'Novelki'; + icon = 'src/pl/novelki/icon.png'; + site = 'https://novelki.pl'; + version = '1.0.3'; + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + //So far is working, If the author of the site changes it, it will have to be changed. + let link = this.site + '/projekty?filter=t'; + link += '&genre=' + filters?.genres.value; + link += '&status=' + filters?.status.value; + link += '&type=' + filters?.type.value; + link += '&page=' + page; + + const body = await fetchApi(link).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to load novels (open in web view and login)'); + return res.text(); + }); + const loadedCheerio = parseHTML(body); + const load = loadedCheerio('#projects > div'); + + //if(load.length == 0) throw new Error("Failed to load page (open in web view and login)"); + + const novels: Plugin.NovelItem[] = load + .map((index, element) => ({ + name: loadedCheerio(element) + .find('.card-title') + .attr('title') as string, + cover: + this.site + loadedCheerio(element).find('.card-img-top').attr('src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + }; + + loadedCheerio('p.h5').each((i, e) => { + const text = loadedCheerio(e).text().trim(); + if (text.includes('Autor:')) novel.author = text.split(':')[1].trim(); + if (text.includes('Status projektu:')) { + switch (`${text.split(':')[1].trim()}`.toLowerCase()) { + case 'wstrzymany': + novel.status = NovelStatus.OnHiatus; + break; + case 'zakończony': + novel.status = NovelStatus.Completed; + break; + case 'aktywny': + novel.status = NovelStatus.Ongoing; + break; + case 'porzucony': + novel.status = NovelStatus.Cancelled; + break; + case 'zlicencjonowany': + novel.status = NovelStatus.Licensed; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + }); + novel.name = loadedCheerio('div.col-sm-12.col-md-6.col-lg-8.mb-5') + .find('h3') + .text() + .trim(); + novel.cover = this.site + loadedCheerio('.img-fluid').attr('src'); // TODO: return not only undefined + + novel.genres = loadedCheerio('span.badge') + .map((i, e) => loadedCheerio(e).text()) + .get() + .join(', '); + + novel.summary = loadedCheerio('.h5:contains("Opis:")') + .next('p') + .next('p') + .text() + .trim(); + + //TODO: Dodać tłumacza danej serii :D + + const chapters: Plugin.ChapterItem[] = []; + + const chaptersList = loadedCheerio('.chapters > .col-md-3 > div').get(); + chaptersList.forEach((e, i) => { + const urlChapters = loadedCheerio(e).find('a').attr('href') || ''; + + const chapter: Plugin.ChapterItem = { + name: loadedCheerio(e).find('a')?.text().trim(), + path: urlChapters, + releaseTime: loadedCheerio(e) + .find('.card-footer > span') + .text() + .trim() + .split('-') + .reverse() + .join('-'), + chapterNumber: chaptersList.length - i, + }; + chapters.push(chapter); + }); + + novel.chapters = chapters.reverse(); + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const pattern = /\/projekty\/([^/]+)\/([^/]+)/; + const codeChapter = pattern.exec(chapterPath) || ''; + const body = await fetchApi( + `${this.site}/api/reader/chapters/${codeChapter[2]}`, + ).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to load chapter (open in web view and login)'); + return res.json(); + }); + + const chapterText = body.data.content; + return chapterText; + } + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const body = await fetchApi( + `${this.site}/projekty?filter=t&title=${searchTerm}+&page=${page}`, + ).then(res => { + if (res.url == `${this.site}/guest`) + throw new Error('Failed to search novels (open in web view and login)'); + return res.text(); + }); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = loadedCheerio('#projects > div') + .map((index, element) => ({ + name: loadedCheerio(element) + .find('.card-title') + .attr('title') as string, + cover: + this.site + loadedCheerio(element).find('.card-img-top').attr('src'), + path: loadedCheerio(element).find('a').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + + return novels; + } + + filters = { + genres: { + value: '', + label: 'Genre', + options: [ + { label: 'Wybierz gatunek', value: '' }, + { label: 'Adaptacja do anime', value: '35' }, + { label: 'Adaptacja do dramy', value: '40' }, + { label: 'Adaptacja do mangi', value: '36' }, + { label: 'Akcja', value: '1' }, + { label: 'Boys Love', value: '22' }, + { label: 'Bromans', value: '33' }, + { label: 'Dojrzały', value: '11' }, + { label: 'Dramat', value: '10' }, + { label: 'Ecchi', value: '29' }, + { label: 'Fantasy', value: '2' }, + { label: 'Gender Bender', value: '28' }, + { label: 'Girls Love', value: '23' }, + { label: 'Harem', value: '8' }, + { label: 'Hentai', value: '43' }, + { label: 'Historyczny', value: '30' }, + { label: 'Horror', value: '15' }, + { label: 'Isekai', value: '44' }, + { label: 'Josei', value: '27' }, + { label: 'Komedia', value: '3' }, + { label: 'Mecha', value: '16' }, + { label: 'Nadprzyrodzone', value: '9' }, + { label: 'Okruchy życia', value: '18' }, + { label: 'Oneshot', value: '41' }, + { label: 'Parodia', value: '19' }, + { label: 'Przygodowy', value: '4' }, + { label: 'Psychologiczny', value: '12' }, + { label: 'Reinkarnacja', value: '42' }, + { label: 'Romans', value: '7' }, + { label: 'RPG', value: '17' }, + { label: 'Sci-fi', value: '21' }, + { label: 'Seinen', value: '26' }, + { label: 'Shoujo', value: '25' }, + { label: 'Shounen', value: '24' }, + { label: 'Smut', value: '31' }, + { label: 'Sport', value: '32' }, + { label: 'Świat gry', value: '5' }, + { label: 'Szkolne życie', value: '20' }, + { label: 'Sztuki walki', value: '6' }, + { label: 'Tajemnica', value: '14' }, + { label: 'Tragedia', value: '13' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Status', + value: '', + options: [ + { label: 'Wybierz status', value: '' }, + { label: 'Zakończony', value: '0' }, + { label: 'Aktywny', value: '1' }, + { label: 'Porzucony', value: '2' }, + { label: 'Wstrzymany', value: '3' }, + { label: 'Zlicencjonowany', value: '4' }, + ], + type: FilterTypes.Picker, + }, + type: { + label: 'Type', + value: '', + options: [ + { label: 'Wybierz typ', value: '' }, + { label: 'Nie zdefiniowano', value: '0' }, + { label: 'Light Novel', value: '1' }, + { label: 'Wuxia', value: '2' }, + { label: 'Xianxia', value: '3' }, + { label: 'Web Novel', value: '4' }, + { label: 'Autorska', value: '5' }, + { label: 'Inne', value: '6' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new NovelkiPL(); diff --git a/plugins/portuguese/.gitkeep b/plugins/portuguese/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/portuguese/blogdoamonnovels.ts b/plugins/portuguese/blogdoamonnovels.ts new file mode 100644 index 000000000..ec058e72a --- /dev/null +++ b/plugins/portuguese/blogdoamonnovels.ts @@ -0,0 +1,266 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; + +type BloggerEntry = { + title: { $t: string }; + link: { rel: string; href: string }[]; + media$thumbnail?: { url: string }; + updated?: { $t: string }; + content?: { $t: string }; +}; + +class BlogDoAnonNovelsPlugin implements Plugin.PluginBase { + id = 'blogdoamonnovels'; + name = 'Blog do Amon Novels'; + version = '1.0.1'; + icon = 'src/pt-br/blogdoamonnovels/icon.png'; + site = 'https://www.blogdoamonnovels.com'; + + parseNovels(json: string) { + const novels: Plugin.NovelItem[] = []; + + const result = JSON.parse(json); + + if (!('entry' in result.feed)) { + return novels; + } + + result.feed.entry.forEach((n: BloggerEntry) => { + const novelName: string = n.title.$t; + + const novelUrl = n.link.find( + (t: { rel: string }) => 'alternate' == t.rel, + )?.href; + if (!novelUrl) return; + + const coverUrl = n.media$thumbnail?.url.replace('/s72-c/', '/w340/'); + + const novel = { + name: novelName, + cover: coverUrl || defaultCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + if (showLatestNovels) { + return this.searchNovels('', pageNo); + } + + if (pageNo > 1) { + return []; + } + const body = await fetchApi(this.site).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.PopularPosts article').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3 a').text().trim(); + const novelUrl = loadedCheerio(ele).find('h3 a').attr('href'); + const coverUrl = loadedCheerio(ele).find('img').attr('src'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: coverUrl || defaultCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('[itemprop="name"]').text() || 'Untitled', + cover: loadedCheerio('img[itemprop="image"]').attr('src'), + summary: loadedCheerio('#synopsis') + .find('br') + .replaceWith('\n') + .end() + .text() + .trim(), + chapters: [], + }; + + novel.author = loadedCheerio('#extra-info dl:contains("Autor") dd') + .text() + .trim(); + + novel.artist = loadedCheerio('#extra-info dl:contains("Artista") dd') + .text() + .trim(); + + novel.status = loadedCheerio('[data-status]').text().trim(); + + novel.genres = loadedCheerio('dt:contains("Gênero:")') + .parent() + .find('a') + .map((_, ex) => loadedCheerio(ex).text().trim()) + .toArray() + .join(','); + + const cat = loadedCheerio('#clwd').text().split("'")[1]; + + if (!cat) { + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('#chapters chapter').each((idx, ele) => { + const chapterName = loadedCheerio(ele).find('a').text().trim(); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + if (!chapterUrl) return; + + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + + novel.chapters = chapters.reverse().map((c, i) => ({ + ...c, + name: c.name + ` - Ch. ${i + 1}`, + chapterNumber: i + 1, + })); + + if (!novel.summary) { + const $summary = loadedCheerio('#chapters'); + $summary.find('h3').remove(); + $summary.find('div.flex').remove(); + $summary.find('div.separator').remove(); + $summary.find('#custom-hero').remove(); + $summary.find('[id=listItem]').remove(); + novel.summary = $summary.text().trim(); + } + + return novel; + } + + const maxResults = 150; + let startIndex = 1; + let length = 0; + + const chapters: Plugin.ChapterItem[] = []; + do { + const jsonUrl = `${this.site}/feeds/posts/default/-/${cat}?alt=json&start-index=${startIndex}&max-results=${maxResults}`; + const bodyResponse = await fetchApi(jsonUrl).then(result => + result.text(), + ); + + const result = JSON.parse(bodyResponse); + + if (!('entry' in result.feed)) { + break; + } + + result.feed.entry.forEach((n: BloggerEntry) => { + let chapterName: string = n.title.$t; + + // Skip self + if (chapterName === novel.name) { + return; + } + + // const chapterNumber: number = parseFloat(chapterName.split(' ', 2)[1]); + const chapterUrl = n.link.find( + (t: { rel: string }) => 'alternate' == t.rel, + )?.href; + const releaseTime = n.updated ? new Date(n.updated.$t) : null; + if (!chapterUrl) return; + + if (n.content && n.content.$t) { + try { + const dom = parseHTML(n.content.$t); + chapterName = dom('.conteudo_teste center h1').text().trim(); + } catch (error) { + /* empty */ + } + } + + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: releaseTime?.toISOString(), + // chapterNumber: chapterNumber, + }); + }); + + length = result.feed.entry.length; + startIndex += maxResults; + } while (length >= maxResults); + + novel.chapters = chapters.reverse().map((c, i) => ({ + ...c, + name: c.name + ` - Ch. ${i + 1}`, + chapterNumber: i + 1, + })); + return novel; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams(); + const maxResults = 10; + + params.append('alt', 'json'); + if (pageNo > 1) { + params.append('start-index', `${(pageNo - 1) * maxResults + 1}`); + } + params.append('max-results', `${maxResults}`); + params.append('q', `label:Series ${searchTerm}`.trim()); + + const jsonUrl = `${this.site}/feeds/posts/summary?` + params.toString(); + const json = await fetchApi(jsonUrl).then(result => result.text()); + + return this.parseNovels(json); + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const $readerarea = loadedCheerio('.conteudo_teste'); + + // Remove empty paragraphs + $readerarea.find('p').each((i, el) => { + const $this = loadedCheerio(el); + const $imgs = $this.find('img'); + const cleanContent = $this + .text() + ?.replace(/\s| /g, '') + ?.replace(this.site, ''); + + // Without images and empty content + if ($imgs?.length === 0 && cleanContent?.length === 0) { + $this.remove(); + } + }); + + return $readerarea.html() || ''; + } + + filters = {} satisfies Filters; +} + +export default new BlogDoAnonNovelsPlugin(); diff --git a/plugins/portuguese/illusia.ts b/plugins/portuguese/illusia.ts new file mode 100644 index 000000000..eaf423857 --- /dev/null +++ b/plugins/portuguese/illusia.ts @@ -0,0 +1,329 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { load as loadCheerio } from 'cheerio'; +import { NovelStatus } from '@libs/novelStatus'; +import { defaultCover } from '@libs/defaultCover'; + +class Illusia implements Plugin.PluginBase { + id = 'illusia'; + name = 'Illusia'; + icon = 'src/pt-br/illusia/icon.png'; + site = 'https://illusia.com.br'; + version = '1.0.2'; + filters: Filters | undefined = undefined; + + headers = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36', + }; + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const orderBy = showLatestNovels ? 'modified' : 'comment_count'; + const pagePath = pageNo === 1 ? '' : `page/${pageNo}/`; + + const url = `${this.site}/${pagePath}?s=&post_type=fcn_story&sentence=0&orderby=${orderBy}&order=desc&age_rating=Any&story_status=Any&miw=0&maw=0&genres=&fandoms=&characters=&tags=&warnings=&authors=&ex_genres=&ex_fandoms=&ex_characters=&ex_tags=&ex_warnings=&ex_authors=`; + + const req = await fetchApi(url, { headers: this.headers }); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + const novels = loadedCheerio( + '#search-result-list > li, article.story, article.post, .card, .story-card, .ranking-item, ul.ranking-list li, .bsx, .book-item, .fcn-story', + ) + .map((i, el) => { + const item = loadedCheerio(el); + const titleEl = item + .find( + '.card__title a, h2 a, h3 a, h4 a, .card-title a, .story-title a, .story__title a, .ranking-title a, .entry-title a, .tt', + ) + .first(); + const novelName = titleEl.text().trim(); + const novelUrl = + titleEl.attr('href') || item.find('a').first().attr('href'); + + let novelCover = + item.find('img').attr('data-src') || + item.find('img').attr('data-lazy-src') || + item.find('img').attr('src') || + item.find('.ranking-cover, .story-cover, .img-cover').attr('data-bg'); + + if (!novelCover) { + const bgElement = item.find('[style*="url("]'); + const styleAttr = bgElement.length + ? bgElement.attr('style') + : item.attr('style'); + + if (styleAttr) { + const match = styleAttr.match(/url\(['"]?([^'"]+)['"]?\)/i); + if (match) novelCover = match[1]; + } + } + + if (!novelName || !novelUrl) return null; + + if (novelCover && novelCover.startsWith('/')) { + novelCover = this.site + novelCover; + } + + return { + name: novelName, + cover: novelCover || defaultCover, + path: novelUrl + .replace(this.site, '') + .replace(/^\//, '') + .replace(/\/$/, ''), + } as Plugin.NovelItem; + }) + .toArray() + .filter(novel => novel !== null) as Plugin.NovelItem[]; + + const uniqueNovels = Array.from( + new Map(novels.map(item => [item.path, item])).values(), + ); + return uniqueNovels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const req = await fetchApi(`${this.site}/${novelPath}/`, { + headers: this.headers, + }); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.story__identity-title, h1.post-title') + .text() + .trim(), + }; + + let author = + loadedCheerio( + 'span.custom-story-info a.author, a[href*="/author/"], a[rel="author"]', + ) + .first() + .text() + .trim() || + loadedCheerio( + '.story__author, .story-author, .author-name, .post-author, [class*="__author"]', + ) + .first() + .text() + .trim(); + + if (!author) { + const metaText = loadedCheerio( + '.story__identity-meta, .story-meta, .custom-story-info', + ) + .text() + .trim(); + if (metaText) { + author = metaText + .split('|')[0] + .replace(/^(Autor[a]?|Por|Author|by)[\s:]*/i, '') + .trim(); + } + } + novel.author = author || 'Desconhecido'; + + novel.cover = + loadedCheerio('figure.story__thumbnail img').attr('data-src') || + loadedCheerio('figure.story__thumbnail img').attr('src') || + loadedCheerio('.story__thumbnail img').attr('data-src') || + loadedCheerio('.story__thumbnail img').attr('src') || + loadedCheerio('figure.story__thumbnail > a').attr('href') || + defaultCover; + + novel.genres = loadedCheerio( + 'div.tag-group > a, section.tag-group > a, .genres a', + ) + .map((i, el) => loadedCheerio(el).text().trim()) + .toArray() + .join(','); + + let summaryHtml = + loadedCheerio( + 'section.story__summary, div.story__summary, .summary', + ).html() || ''; + summaryHtml = summaryHtml + .replace(/<br\s*\/?>/gi, '\n') + .replace(/<\/p>/gi, '\n\n') + .replace(/<\/div>/gi, '\n'); + novel.summary = loadCheerio(summaryHtml) + .text() + .trim() + .replace(/\n{3,}/g, '\n\n'); + + const chapterElements = loadedCheerio( + 'li.chapter-group__list-item, ul.chapter-list li, .chapters li, .chapter-item', + ); + + novel.chapters = chapterElements + .map((i, el) => { + const item = loadedCheerio(el); + + const aTag = item.find('a').first(); + const chapterName = aTag.text().trim(); + const chapterUrl = aTag.attr('href'); + + if (!chapterUrl) return null; + + const chapterNumberMatch = + chapterName.match(/(?:cap[íi]tulo|cap\.?|ch\.?)\s*(\d+(\.\d+)?)/i) || + chapterName.match(/^(\d+(\.\d+)?)/); + const chapterNumber = chapterNumberMatch + ? Number(chapterNumberMatch[1]) + : undefined; + + const chapter: Plugin.ChapterItem = { + name: chapterName, + path: chapterUrl + .replace(this.site, '') + .replace(/^\//, '') + .replace(/\/$/, ''), + }; + + if (chapterNumber !== undefined) { + chapter.chapterNumber = chapterNumber; + } + + return chapter; + }) + .toArray() + .filter(chapter => chapter !== null) as Plugin.ChapterItem[]; + + const metaBlockText = + loadedCheerio('div.story__identity-meta, .story-meta').text() || ''; + const metaParts = metaBlockText.split('|').map(p => p.trim()); + + let statusText = loadedCheerio('span.story__status') + .text() + .trim() + .toLowerCase(); + if (!statusText && metaParts.length > 1) { + statusText = metaBlockText.toLowerCase(); + } + + if ( + statusText.includes('ongoing') || + statusText.includes('andamento') || + statusText.includes('lançando') || + statusText.includes('ativa') + ) + novel.status = NovelStatus.Ongoing; + else if ( + statusText.includes('completed') || + statusText.includes('completo') + ) + novel.status = NovelStatus.Completed; + else if ( + statusText.includes('cancelled') || + statusText.includes('cancelado') || + statusText.includes('dropado') + ) + novel.status = NovelStatus.Cancelled; + else if ( + statusText.includes('hiatus') || + statusText.includes('hiato') || + statusText.includes('pausado') + ) + novel.status = NovelStatus.OnHiatus; + else novel.status = NovelStatus.Unknown; + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const req = await fetchApi(`${this.site}/${chapterPath}/`, { + headers: this.headers, + }); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + const chapterContent = loadedCheerio( + 'section#chapter-content > div, div.chapter-content', + ); + chapterContent + .find( + 'script, style, iframe, .patreon-popup, .fcn-notice, .fictioneer-notice, div.card', + ) + .remove(); + + return chapterContent.html() || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const pagePath = pageNo === 1 ? '' : `page/${pageNo}/`; + + const url = `${this.site}/${pagePath}?s=${encodeURIComponent(searchTerm)}&post_type=fcn_story&sentence=0&orderby=relevance&order=desc&age_rating=Any&story_status=Any&miw=0&maw=0&genres=&fandoms=&characters=&tags=&warnings=&authors=&ex_genres=&ex_fandoms=&ex_characters=&ex_tags=&ex_warnings=&ex_authors=`; + + const req = await fetchApi(url, { headers: this.headers }); + const body = await req.text(); + const loadedCheerio = loadCheerio(body); + + const novels = loadedCheerio( + '#search-result-list > li, article.story, article.post, .card, .story-card, .ranking-item, ul.ranking-list li, .bsx, .book-item, .fcn-story', + ) + .map((i, el) => { + const item = loadedCheerio(el); + const titleEl = item + .find( + '.card__title a, h2 a, h3 a, h4 a, .card-title a, .story-title a, .story__title a, .ranking-title a, .entry-title a, .tt', + ) + .first(); + const novelName = titleEl.text().trim(); + const novelUrl = + titleEl.attr('href') || item.find('a').first().attr('href'); + + let novelCover = + item.find('img').attr('data-src') || + item.find('img').attr('data-lazy-src') || + item.find('img').attr('src') || + item.find('.ranking-cover, .story-cover, .img-cover').attr('data-bg'); + + if (!novelCover) { + const bgElement = item.find('[style*="url("]'); + const styleAttr = bgElement.length + ? bgElement.attr('style') + : item.attr('style'); + if (styleAttr) { + const match = styleAttr.match(/url\(['"]?([^'"]+)['"]?\)/i); + if (match) novelCover = match[1]; + } + } + + if (!novelName || !novelUrl) return null; + + if (novelCover && novelCover.startsWith('/')) { + novelCover = this.site + novelCover; + } + + return { + name: novelName, + cover: novelCover || defaultCover, + path: novelUrl + .replace(this.site, '') + .replace(/^\//, '') + .replace(/\/$/, ''), + } as Plugin.NovelItem; + }) + .toArray() + .filter(novel => novel !== null) as Plugin.NovelItem[]; + + const uniqueNovels = Array.from( + new Map(novels.map(item => [item.path, item])).values(), + ); + return uniqueNovels; + } + + // resolveUrl = (path: string, isNovel?: boolean) => `${this.site}/${path}/`; +} + +export default new Illusia(); diff --git a/plugins/portuguese/novelmania.ts b/plugins/portuguese/novelmania.ts new file mode 100644 index 000000000..d3cda7130 --- /dev/null +++ b/plugins/portuguese/novelmania.ts @@ -0,0 +1,228 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class NovelMania implements Plugin.PluginBase { + id = 'novelmania.com.br'; + name = 'Novel Mania'; + icon = 'src/pt-br/novelmania/icon.png'; + site = 'https://novelmania.com.br'; + version = '1.0.1'; + imageRequestInit?: Plugin.ImageRequestInit | undefined = undefined; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = `${this.site}/novels?titulo=`; + url += `&categoria=${filters?.genres.value}`; + url += `&status=${filters?.status.value}`; + url += `&nacionalidade=${filters?.type.value}`; + url += `&ordem=${filters?.ordem.value}`; + url += `&page%5Bpage%5D=${pageNo}`; + + const body = await fetchApi(url).then(res => res.text()); + + const loadedCheerio = parseHTML(body); + const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2'); + + const novels: Plugin.NovelItem[] = load + .map((index, element) => ({ + name: loadedCheerio(element).find('a.novel-title > h5').text(), + cover: loadedCheerio(element) + .find('a > div.card.c-size-1.border > img.card-image') + .attr('src'), + path: loadedCheerio(element).find('a.novel-title').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: + loadedCheerio( + 'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1', + ) + .text() + .trim() || 'Sem título', + }; + + loadedCheerio('b').remove(); + + novel.name = + loadedCheerio( + 'div.col-md-8 > div.novel-info > div.d-flex.flex-row.align-items-center > h1', + ) + .text() + .trim() || 'Sem título'; + novel.summary = + loadedCheerio('div.tab-pane.fade.show.active > div.text > p') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join('\n\n') + .trim() || ''; + novel.cover = + loadedCheerio('div.novel-img > img.img-responsive').attr('src') || + defaultCover; + novel.author = loadedCheerio('div.novel-info > span.authors.mb-1') + .text() + .trim(); + novel.genres = loadedCheerio('div.tags > ul.list-tags.mb-0 > li > a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + const status = loadedCheerio('div.novel-info > span.authors.mb-3') + .text() + .trim(); + switch (status) { + case 'Ativo': + novel.status = NovelStatus.Ongoing; + break; + case 'Pausado': + novel.status = NovelStatus.OnHiatus; + break; + case 'Completo': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + } + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio( + 'div.accordion.capitulo > div.card > div.collapse > div.card-body.p-0 > ol > li', + ).each((i, el) => { + const chapterName = `${loadedCheerio(el).find('a > span.sub-vol').text().trim()} - ${loadedCheerio(el).find('a > strong').text().trim()}`; + const chapterPath = loadedCheerio(el).find('a').attr('href'); + if (chapterPath) chapters.push({ name: chapterName, path: chapterPath }); + }); + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const response = await fetchApi(`${this.site}${chapterPath}`).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(response); + return loadedCheerio('div#chapter-content').html() || ''; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/novels?titulo=${searchTerm}&page%5Bpage%5D=${pageNo}`; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const load = loadedCheerio('div.top-novels.dark.col-6 > div.row.mb-2'); + + const novels: Plugin.NovelItem[] = load + .map((index, element) => ({ + name: loadedCheerio(element).find('a.novel-title > h5').text(), + cover: loadedCheerio(element) + .find('a > div.card.c-size-1.border > img.card-image') + .attr('src'), + path: loadedCheerio(element).find('a.novel-title').attr('href') || '', + })) + .get() + .filter(novel => novel.name && novel.path); + return novels; + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/chapter/') + path; + + filters = { + genres: { + value: '', + label: 'Gêneros', + options: [ + { label: 'Todos', value: '' }, + { label: 'Ação', value: '01' }, + { label: 'Adulto', value: '02' }, + { label: 'Artes Marciais', value: '07' }, + { label: 'Aventura', value: '03' }, + { label: 'Comédia', value: '04' }, + { label: 'Cotidiano', value: '16' }, + { label: 'Drama', value: '23' }, + { label: 'Ecchi', value: '27' }, + { label: 'Erótico', value: '22' }, + { label: 'Escolar', value: '13' }, + { label: 'Fantasia', value: '05' }, + { label: 'Harém', value: '21' }, + { label: 'Isekai', value: '30' }, + { label: 'Magia', value: '26' }, + { label: 'Mecha', value: '08' }, + { label: 'Medieval', value: '31' }, + { label: 'Militar', value: '24' }, + { label: 'Mistério', value: '09' }, + { label: 'Mitologia', value: '10' }, + { label: 'Psicológico', value: '11' }, + { label: 'Realidade Virtual', value: '36' }, + { label: 'Romance', value: '12' }, + { label: 'Sci-fi', value: '14' }, + { label: 'Sistema de Jogo', value: '15' }, + { label: 'Sobrenatural', value: '17' }, + { label: 'Suspense', value: '29' }, + { label: 'Terror', value: '06' }, + { label: 'Wuxia', value: '18' }, + { label: 'Xianxia', value: '19' }, + { label: 'Xuanhuan', value: '20' }, + { label: 'Yaoi', value: '35' }, + { label: 'Yuri', value: '37' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Status', + value: '', + options: [ + { label: 'Todos', value: '' }, + { label: 'Ativo', value: 'ativo' }, + { label: 'Completo', value: 'Completo' }, + { label: 'Pausado', value: 'pausado' }, + { label: 'Parado', value: 'Parado' }, + ], + type: FilterTypes.Picker, + }, + type: { + label: 'Type', + value: '', + options: [ + { label: 'Todas', value: '' }, + { label: 'Americana', value: 'americana' }, + { label: 'Angolana', value: 'angolana' }, + { label: 'Brasileira', value: 'brasileira' }, + { label: 'Chinesa', value: 'chinesa' }, + { label: 'Coreana', value: 'coreana' }, + { label: 'Japonesa', value: 'japonesa' }, + ], + type: FilterTypes.Picker, + }, + ordem: { + label: 'Ordenar', + value: '', + options: [ + { label: 'Qualquer ordem', value: '' }, + { label: 'Ordem alfabética', value: '1' }, + { label: 'Nº de Capítulos', value: '2' }, + { label: 'Popularidade', value: '3' }, + { label: 'Novidades', value: '4' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new NovelMania(); diff --git a/plugins/portuguese/tsundoku.ts b/plugins/portuguese/tsundoku.ts new file mode 100644 index 000000000..5dd4204b5 --- /dev/null +++ b/plugins/portuguese/tsundoku.ts @@ -0,0 +1,290 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import dayjs from 'dayjs'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class TsundokuPlugin implements Plugin.PluginBase { + id = 'tsundoku'; + name = 'Tsundoku Traduções'; + version = '1.0.1'; + icon = 'src/pt-br/tsundoku/icon.png'; + site = 'https://tsundoku.com.br'; + + parseDate(date: string): string { + const monthMapping: Record<string, number> = { + janeiro: 1, + fevereiro: 2, + marco: 3, + abril: 4, + maio: 5, + junho: 6, + julho: 7, + agosto: 8, + setembro: 9, + outubro: 10, + novembro: 11, + dezembro: 12, + }; + const [month, day, year] = date.split(/,?\s+/); + return dayjs( + `${year}-${monthMapping[month.normalize('NFD').replace(/[\u0300-\u036f]/g, '')]}-${day}`, + ).toISOString(); + } + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.listupd .bsx').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.tt').text().trim(); + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + const coverUrl = loadedCheerio(ele).find('img').attr('src'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: coverUrl || defaultCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams(); + + if (page > 1) { + params.append('page', `${page}`); + } + params.append('type', 'novel'); + + if (showLatestNovels) { + params.append('order', 'latest'); + } else if (filters) { + if (filters.genre.value.length) { + filters.genre.value.forEach(value => { + params.append('genre[]', value); + }); + } + params.append('order', filters.order.value); + } + + const url = `${this.site}/manga/?` + params.toString(); + + const body = await fetchApi(url).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title').text() || 'Untitled', + cover: loadedCheerio('.main-info .thumb img').attr('src'), + summary: loadedCheerio('.entry-content.entry-content-single div:eq(0)') + .text() + .trim(), + chapters: [], + }; + + novel.author = loadedCheerio('.tsinfo .imptdt:contains("Autor")') + .text() + .replace('Autor ', '') + .trim(); + + novel.artist = loadedCheerio('.tsinfo .imptdt:contains("Artista")') + .text() + .replace('Artista ', '') + .trim(); + + novel.status = loadedCheerio('.tsinfo .imptdt:contains("Status")') + .text() + .replace('Status ', '') + .trim(); + + novel.genres = loadedCheerio('.mgen a') + .map((_, ex) => loadedCheerio(ex).text()) + .toArray() + .join(','); + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('#chapterlist ul > li').each((idx, ele) => { + const chapterName = loadedCheerio(ele).find('.chapternum').text().trim(); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + const releaseDate = loadedCheerio(ele).find('.chapterdate').text(); + if (!chapterUrl) return; + + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: this.parseDate(releaseDate), + }); + }); + + novel.chapters = chapters.reverse().map((c, i) => ({ + ...c, + name: c.name + ` - Ch. ${i + 1}`, + chapterNumber: i + 1, + })); + return novel; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams(); + + if (pageNo > 1) { + params.append('page', `${pageNo}`); + } + params.append('type', 'novel'); + params.append('title', searchTerm); + + const url = `${this.site}/manga/?` + params.toString(); + + const body = await fetchApi(url).then(result => result.text()); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const chapterTitle = loadedCheerio('.headpost .entry-title').text(); + const novelTitle = loadedCheerio('.headpost a').text(); + const title = chapterTitle + .replace(novelTitle, '') + .replace(/^\W+/, '') + .trim(); + + const spoilerContent = loadedCheerio( + '#readerarea .collapseomatic_content', + ).html(); + if (spoilerContent) { + return `<h1>${title}</h1>\n${spoilerContent}`; + } + + const $readerarea = loadedCheerio('#readerarea'); + $readerarea.find('img.wp-image-15656').remove(); // Remove logo messages + + // Remove empty paragraphs + $readerarea.find('p').each((i, el) => { + const $this = loadedCheerio(el); + const $imgs = $this.find('img'); + const cleanContent = $this + .text() + ?.replace(/\s| /g, '') + ?.replace(this.site, ''); + + // Without images and empty content + if ($imgs?.length === 0 && cleanContent?.length === 0) { + $this.remove(); + } + }); + + const chapterText = $readerarea.html() || ''; + const parts = chapterText.split(/<hr ?\/?>/); + const lastPart = parts[parts.length - 1]; + if (parts.length > 1 && lastPart.includes('https://discord')) { + parts.pop(); + } + + return `<h1>${title}</h1>\n${parts.join('<hr />')}`; + } + + filters = { + order: { + label: 'Ordenar por', + value: '', + options: [ + { label: 'Padrão', value: '' }, + { label: 'A-Z', value: 'title' }, + { label: 'Z-A', value: 'titlereverse' }, + { label: 'Atualizar', value: 'update' }, + { label: 'Adicionar', value: 'latest' }, + { label: 'Popular', value: 'popular' }, + ], + type: FilterTypes.Picker, + }, + genre: { + label: 'Gênero', + value: [], + options: [ + { label: 'Ação', value: '328' }, + { label: 'Adult', value: '343' }, + { label: 'Anatomia', value: '408' }, + { label: 'Artes Marciais', value: '340' }, + { label: 'Aventura', value: '315' }, + { label: 'Ciência', value: '398' }, + { label: 'Comédia', value: '322' }, + { label: 'Comédia Romântica', value: '378' }, + { label: 'Cotidiano', value: '399' }, + { label: 'Drama', value: '311' }, + { label: 'Ecchi', value: '329' }, + { label: 'Fantasia', value: '316' }, + { label: 'Feminismo', value: '362' }, + { label: 'Gender Bender', value: '417' }, + { label: 'Guerra', value: '368' }, + { label: 'Harém', value: '350' }, + { label: 'Hentai', value: '344' }, + { label: 'História', value: '400' }, + { label: 'Histórico', value: '380' }, + { label: 'Horror', value: '317' }, + { label: 'Humor Negro', value: '363' }, + { label: 'Isekai', value: '318' }, + { label: 'Josei', value: '356' }, + { label: 'Joshikousei', value: '364' }, + { label: 'LitRPG', value: '387' }, + { label: 'Maduro', value: '351' }, + { label: 'Mágia', value: '372' }, + { label: 'Mecha', value: '335' }, + { label: 'Militar', value: '414' }, + { label: 'Mistério', value: '319' }, + { label: 'Otaku', value: '365' }, + { label: 'Psicológico', value: '320' }, + { label: 'Reencarnação', value: '358' }, + { label: 'Romance', value: '312' }, + { label: 'RPG', value: '366' }, + { label: 'Sátira', value: '367' }, + { label: 'Sci-fi', value: '371' }, + { label: 'Seinen', value: '326' }, + { label: 'Sexo Explícito', value: '345' }, + { label: 'Shoujo', value: '323' }, + { label: 'Shounen', value: '341' }, + { label: 'Slice-of-Life', value: '324' }, + { label: 'Sobrenatural', value: '359' }, + { label: 'Supernatural', value: '401' }, + { label: 'Suspense', value: '407' }, + { label: 'Thriller', value: '410' }, + { label: 'Tragédia', value: '352' }, + { label: 'Vida Escolar', value: '331' }, + { label: 'Webtoon', value: '381' }, + { label: 'Xianxia', value: '357' }, + { label: 'Xuanhuan', value: '395' }, + { label: 'Yuri', value: '313' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new TsundokuPlugin(); diff --git a/plugins/russian/LitSpace.broken.ts b/plugins/russian/LitSpace.broken.ts new file mode 100644 index 000000000..b32ab5e5a --- /dev/null +++ b/plugins/russian/LitSpace.broken.ts @@ -0,0 +1,314 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; + +const headers: Record<string, string> = { + 'Content-Type': 'application/json', + 'X-Inertia': 'true', + 'X-Inertia-Version': '6666cd76f96956469e7be39d750cc7d9', +}; + +class freedlit implements Plugin.PluginBase { + id = 'freedlit.space'; + name = 'LitSpace'; + site = 'https://freedlit.space'; + version = '1.1.0'; + icon = 'src/ru/freedlit/icon.png'; + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/get-books/?sort='; + url += showLatestNovels ? 'recent' : filters?.sort?.value || 'popular'; + url += '&access=' + (filters?.access?.value || 'all'); + url += '&hideAdult=' + (filters?.hideAdult?.value || false); + url += '&page=' + page; + const novels: Plugin.NovelItem[] = []; + + const { books }: { books: Books } = await fetchApi(url).then( + (res: Response) => { + this.getToken(res.headers); + return res.json(); + }, + ); + + books.data.forEach(novel => + novels.push({ + name: novel.title, + cover: novel.cover + ? this.site + '/storage/' + novel.cover + : defaultCover, + path: novel.item_id.toString(), + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const { + props: { book }, + }: { props: { book: DataEntity } } = await fetchApi( + this.resolveUrl(novelPath, true), + { + headers, + Referer: this.resolveUrl(novelPath, true), + }, + ).then((res: Response) => { + this.getToken(res.headers); + return res.json(); + }); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: book.title, + cover: book.cover ? this.site + '/storage/' + book.cover : defaultCover, + summary: book.annotation, + author: book.authors_names?.[0]?.name || '', + genres: book.tags?.map?.(tags => tags.name)?.join?.(', ') || '', + }; + + const { success }: { success: { items: chaptersData[] } } = await fetchApi( + this.site + '/api/bookpage/get-chapters', + { + method: 'post', + headers, + Referer: this.resolveUrl(novelPath), + body: JSON.stringify({ book_id: novelPath }), + }, + ).then((res: Response) => { + this.getToken(res.headers); + return res.json(); + }); + const chapters: Plugin.ChapterItem[] = []; + + if (success?.items?.length) { + success.items.forEach((chapter, chapterIndex) => + chapters.push({ + name: chapter.header, + path: novelPath + '/' + chapter.id, + releaseTime: chapter.first_published_formated, + chapterNumber: chapterIndex + 1, + }), + ); + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [book_id, chapter_id] = chapterPath.split('/'); + if (!headers['X-XSRF-TOKEN']) { + await fetchApi(this.site).then((res: Response) => + this.getToken(res.headers), + ); + } + + const { success }: { success: chapterContent } = await fetchApi( + this.site + '/reader/get-content', + { + method: 'post', + headers, + Referer: this.resolveUrl(chapterPath), + body: JSON.stringify({ book_id, chapter_id }), + }, + ).then((res: Response) => { + this.getToken(res.headers); + return res.json(); + }); + + const chapterText = success.content; + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const { success }: { success: book[] } = await fetchApi( + this.site + '/api/search?query=' + searchTerm, + ).then((res: Response) => { + this.getToken(res.headers); + return res.json(); + }); + const novels: Plugin.NovelItem[] = []; + + if (success?.length) { + success.forEach(novel => { + if (novel.type === 'book' && novel.title) { + novels.push({ + name: novel.title, + cover: novel.cover + ? this.site + '/storage/' + novel.cover + : defaultCover, + path: novel.id.toString(), + }); + } + }); + } + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/reader/') + path; + + getToken = (header: Headers) => { + const cookies = header.get('set-cookie') || ''; + for (const cookie of cookies.split('; ')) { + const [key, val] = cookie.split('='); + if (key === 'XSRF-TOKEN') { + headers['X-XSRF-TOKEN'] = decodeURIComponent(val); + return; + } + } + if (!headers['X-XSRF-TOKEN']) throw new Error('Failed to find the token'); + }; + + filters = { + sort: { + label: 'Сортировка:', + value: 'popular', + options: [ + { label: 'По популярности', value: 'popular' }, + { label: 'последние обновления', value: 'updated' }, + { label: 'По новизне', value: 'recent' }, + { label: 'По просмотрам', value: 'views' }, + { label: 'По количеству лайков', value: 'likes' }, + ], + type: FilterTypes.Picker, + }, + access: { + label: 'Доступ:', + value: 'all', + options: [ + { label: 'Любой доступ', value: 'all' }, + { label: 'Бесплатные', value: 'free' }, + { label: 'Платные', value: 'paid' }, + ], + type: FilterTypes.Picker, + }, + hideAdult: { + label: 'Скрыть 18+', + value: true, + type: FilterTypes.Switch, + }, + } satisfies Filters; +} + +export default new freedlit(); + +type Books = { + current_page: number; + data: DataEntity[]; + first_page_url: string; + from: number; + last_page: number; + last_page_url: string; + next_page_url: string; + path: string; + per_page: number; + prev_page_url?: null; + to: number; + total: number; +}; +type DataEntity = { + id: number; + item_id: number; + type: string; + form_id: number; + title: string; + authors: string; + cover: string; + access: string; + language: string; + rating: number; + intermediate_rating?: number; + first_published: string; + chapter_updated_at: string; + adults: number; + exclusive: number; + status: number; + total_views: number; + total_likes: number; + show_main_widget: number; + is_audio: boolean; + authors_names?: AuthorsNamesEntity[]; + client_price?: number | null; + client_discount?: null; + seriesBook: boolean; + series_title: string; + main_author_link: string; + publisher_link?: null; + published_chapters_total: number; + total_characters: string; + total_author_sheet: number; + total_critics: number; + total_recommendations: number; + form_name: string; + genres?: GenresEntity[] | null; + tags?: TagsEntity[]; + series_book_num: number; + series_book_id?: number | null; + annotation: string; + total_comments: number; + total_libraries: number; +}; +type AuthorsNamesEntity = { + id: number; + url: string; + name: string; + type: string; +}; +type GenresEntity = { + id: number; +}; +type TagsEntity = { + id: number; + name: string; +}; + +type chaptersData = { + id: number; + header: string; + first_published_formated: string; + intro_snippet_end: number; +}; + +type chapterContent = { + id: number; + book_id: number; + header: string; + content: string; + number: number; + user_files?: null; + intro_snippet_end: number; + status: number; + first_published: number; + auto_publication?: null; + first_published_at?: null; + is_chapter_avalaible: boolean; + total_characters: number; + total_characters_clear: string; +}; + +export type book = { + id: number; + cover?: string | null; + title?: string | null; + type: string; + authors_names?: AuthorsNamesEntity[]; + form_name?: string | null; + relevance: number; + for_blog?: number | null; + for_book?: number | null; + items_count?: number | null; + name?: string | null; + blog_category_id?: number | null; + author_name?: string | null; + user_link?: string | null; + img?: string | null; +}; diff --git a/plugins/russian/authortoday.ts b/plugins/russian/authortoday.ts new file mode 100644 index 000000000..2724c8db0 --- /dev/null +++ b/plugins/russian/authortoday.ts @@ -0,0 +1,455 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { Parser } from 'htmlparser2'; +import dayjs from 'dayjs'; + +class AuthorToday implements Plugin.PluginBase { + id = 'AT'; + name = 'Автор Тудей'; + icon = 'src/ru/authortoday/icon.png'; + site = 'https://author.today'; + version = '1.2.2'; + + private userAgent = + 'Mozilla/5.0 (Android 15; Mobile; rv:138.0) Gecko/138.0 Firefox/138.0'; + + parseNovels(url: string) { + return fetchApi(url, { headers: { 'User-Agent': this.userAgent } }) + .then(res => res.text()) + .then(html => { + const novels: Plugin.NovelItem[] = []; + let tempNovel = {} as Plugin.NovelItem; + let isParsingNovel = false; + let isParsingName = false; + const parser = new Parser({ + onopentag(name, attribs) { + if ( + name === 'a' && + attribs['class'] === 'work-row item-link item-content' && + attribs['href'] + ) { + tempNovel.path = attribs['href'].replace(/\D/g, ''); + isParsingNovel = true; + } + if ( + isParsingNovel && + name === 'h4' && + attribs['class'] === 'work-title' + ) { + isParsingName = true; + } + if ( + isParsingNovel && + name === 'img' && + attribs['alt'] === 'Обложка' + ) { + tempNovel.cover = attribs['data-src']; + } + }, + ontext(data) { + if (isParsingName) { + tempNovel.name = data.trim(); + } + }, + onclosetag(name) { + if (name === 'h4') { + isParsingName = false; + } + if (name === 'a' && isParsingNovel) { + if (!tempNovel.cover) { + tempNovel.cover = defaultCover; + } + novels.push(tempNovel); + tempNovel = {} as Plugin.NovelItem; + isParsingNovel = false; + } + }, + }); + parser.write(html); + parser.end(); + return novels; + }); + } + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/work/genre/' + (filters?.genre?.value || 'all'); + + url += + '?sorting=' + + (showLatestNovels ? 'recent' : filters?.sort?.value || 'popular'); + + if (filters?.form?.value) url += '&form=' + filters.form.value; + if (filters?.state?.value) url += '&state=' + filters.state.value; + if (filters?.series?.value) url += '&series=' + filters.series.value; + if (filters?.access?.value) url += '&access=' + filters.access.value; + if (filters?.promo?.value) url += '&promo=' + filters.promo.value; + url += '&page=' + pageNo; + + return this.parseNovels(url); + } + + async parseNovel(workID: string): Promise<Plugin.SourceNovel> { + const html = await fetchApi(this.resolveUrl(workID, true), { + headers: { 'User-Agent': this.userAgent }, + }).then(res => res.text()); + + const novel: Plugin.SourceNovel = { + path: workID, + name: '', + author: '', + summary: '', + }; + const chapters: Plugin.ChapterItem[] = []; + let isParsingName = false; + let isParsingAuthor = false; + let isReadingSummary = false; + let isParsingGenres = false; + let isParsingStatus = false; + let isParsingChapter = false; + let isReadingChapterName = false; + + let tempChapter = {} as Plugin.ChapterItem; + const parser = new Parser({ + onopentag(name, attribs) { + if (name === 'h1' && attribs['class'] === 'card-title') { + isParsingName = true; + } + if (name === 'img' && attribs['class'] === 'cover-image') { + novel.cover = attribs['src']; + } + if ( + name === 'a' && + attribs['href'] && + attribs['href'].endsWith('/works') + ) { + isParsingAuthor = true; + } + if (name === 'div' && attribs['class'] === 'rich-content') { + isReadingSummary = true; + } + if (name === 'br' && isReadingSummary) { + novel.summary += '\n'; + } + if (name === 'noindex') { + isParsingGenres = true; + if (novel.genres) novel.genres = ''; + } + if ( + name === 'label' && + attribs['class'] && + attribs['class'].includes('label') + ) { + isParsingStatus = true; + } + if (name === 'li' && attribs['class'] === 'clearfix') { + isParsingChapter = true; + } + if (name === 'a' && isParsingChapter && attribs['href']) { + isReadingChapterName = true; + tempChapter.path = attribs.href.replace('/reader/', ''); + } + if (name === 'span' && attribs['data-time']) { + tempChapter.releaseTime = dayjs(attribs['data-time']).format('LLL'); + } + }, + ontext(data) { + if (isParsingName) { + novel.name = data.trim(); + isParsingName = false; + } + if (isParsingAuthor && data.trim()) { + novel.author += data.trim() + ', '; + } + if (isReadingSummary) { + novel.summary += data.trim(); + } + if (isParsingGenres) { + novel.genres += data; + } + if (isParsingStatus) { + const status = data.trim(); + switch (status) { + case 'в процессе': + novel.status = NovelStatus.Ongoing; + break; + case 'весь текст': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + if (isReadingChapterName) { + tempChapter.name = data.trim(); + isReadingChapterName = false; + } + }, + onclosetag(name) { + if (name === 'div' && isParsingAuthor) { + isParsingAuthor = false; + novel.author = novel.author?.slice(0, -2); + } + if (name === 'div' && isReadingSummary) { + isReadingSummary = false; + } + if (name === 'noindex') { + isParsingGenres = false; + } + if (name === 'label') { + isParsingStatus = false; + } + if (name === 'li' && isParsingChapter) { + tempChapter.chapterNumber = chapters.length; + if (tempChapter.path) { + chapters?.push(tempChapter); + } + tempChapter = {} as Plugin.ChapterItem; + isParsingChapter = false; + } + }, + }); + parser.write(html); + parser.end(); + + if (!novel.cover) { + novel.cover = defaultCover; + } + if (!chapters.length) { + chapters.push({ + name: 'Рассказ', + path: workID, + }); + } + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const html = await fetchApi(this.resolveUrl(chapterPath), { + headers: { 'User-Agent': this.userAgent }, + }).then(res => res.text()); + + const [workID, initialChapterID] = chapterPath.split('/'); + let chapterID = initialChapterID; + const userRaw = html.match(/userId:(.*?),/)?.[1]?.trim(); + const userId = userRaw === 'null' ? '' : userRaw; + + if (!chapterID) { + chapterID = html.match(/chapterId:(.*?),/)?.[1]?.trim() || ''; + } + if (!chapterID) throw new Error('Chapter ID not found'); + + const chapter = await fetchApi( + this.site + '/reader/' + workID + '/chapter?id=' + chapterID, + { + headers: { 'User-Agent': this.userAgent }, + }, + ); + const chapterJson: Result = await chapter.json(); + const key = chapter.headers.get('reader-secret'); + + if (!key) + throw new Error('Failed to find the token\n' + chapterJson.messages); + + const chapterText = decrypt(chapterJson.data.text, key, userId); + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number | undefined = 1, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + '/search?category=works&q=' + searchTerm + '&page=' + pageNo; + + return this.parseNovels(url); + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/work/' : '/reader/') + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'popular', + options: [ + { label: 'По популярности', value: 'popular' }, + { label: 'По количеству лайков', value: 'likes' }, + { label: 'По комментариям', value: 'comments' }, + { label: 'По новизне', value: 'recent' }, + { label: 'По просмотрам', value: 'views' }, + { label: 'Набирающие популярность', value: 'trending' }, + ], + type: FilterTypes.Picker, + }, + genre: { + label: 'Жанры', + value: '', + options: [ + { label: 'Все', value: '' }, + { label: 'Альтернативная история', value: 'sf-history' }, + { label: 'Антиутопия', value: 'dystopia' }, + { label: 'Бизнес-литература', value: 'biznes-literatura' }, + { label: 'Боевая фантастика', value: 'sf-action' }, + { label: 'Боевик', value: 'action' }, + { label: 'Боевое фэнтези', value: 'fantasy-action' }, + { label: 'Бояръ-Аниме', value: 'boyar-anime' }, + { label: 'Героическая фантастика', value: 'sf-heroic' }, + { label: 'Героическое фэнтези', value: 'heroic-fantasy' }, + { label: 'Городское фэнтези', value: 'urban-fantasy' }, + { label: 'Детектив', value: 'detective' }, + { label: 'Детская литература', value: 'detskaya-literatura' }, + { label: 'Документальная проза', value: 'non-fiction' }, + { label: 'Историческая проза', value: 'historical-fiction' }, + { label: 'Исторические приключения', value: 'historical-adventure' }, + { label: 'Исторический детектив', value: 'historical-mystery' }, + { label: 'Исторический любовный роман', value: 'historical-romance' }, + { label: 'Историческое фэнтези', value: 'historical-fantasy' }, + { label: 'Киберпанк', value: 'cyberpunk' }, + { label: 'Короткий любовный роман', value: 'short-romance' }, + { label: 'Космическая фантастика', value: 'sf-space' }, + { label: 'ЛитРПГ', value: 'litrpg' }, + { label: 'Любовное фэнтези', value: 'love-fantasy' }, + { label: 'Любовные романы', value: 'romance' }, + { label: 'Мистика', value: 'paranormal' }, + { label: 'Назад в СССР', value: 'back-to-ussr' }, + { label: 'Научная фантастика', value: 'science-fiction' }, + { label: 'Подростковая проза', value: 'teen-prose' }, + { label: 'Политический роман', value: 'political-fiction' }, + { label: 'Попаданцы', value: 'popadantsy' }, + { label: 'Попаданцы в космос', value: 'popadantsy-v-kosmos' }, + { + label: 'Попаданцы в магические миры', + value: 'popadantsy-v-magicheskie-miry', + }, + { label: 'Попаданцы во времени', value: 'popadantsy-vo-vremeni' }, + { label: 'Постапокалипсис', value: 'postapocalyptic' }, + { label: 'Поэзия', value: 'poetry' }, + { label: 'Приключения', value: 'adventure' }, + { label: 'Публицистика', value: 'publicism' }, + { label: 'Развитие личности', value: 'razvitie-lichnosti' }, + { label: 'Разное', value: 'other' }, + { label: 'РеалРПГ', value: 'realrpg' }, + { label: 'Романтическая эротика', value: 'romantic-erotika' }, + { label: 'Сказка', value: 'fairy-tale' }, + { label: 'Современная проза', value: 'modern-prose' }, + { label: 'Современный любовный роман', value: 'contemporary-romance' }, + { label: 'Социальная фантастика', value: 'sf-social' }, + { label: 'Стимпанк', value: 'steampunk' }, + { label: 'Темное фэнтези', value: 'dark-fantasy' }, + { label: 'Триллер', value: 'thriller' }, + { label: 'Ужасы', value: 'horror' }, + { label: 'Фантастика', value: 'sci-fi' }, + { + label: 'Фантастический детектив', + value: 'detective-science-fiction', + }, + { label: 'Фанфик', value: 'fanfiction' }, + { label: 'Фэнтези', value: 'fantasy' }, + { label: 'Шпионский детектив', value: 'spy-mystery' }, + { label: 'Эпическое фэнтези', value: 'epic-fantasy' }, + { label: 'Эротика', value: 'erotica' }, + { label: 'Эротическая фантастика', value: 'sf-erotika' }, + { label: 'Эротический фанфик', value: 'fanfiction-erotika' }, + { label: 'Эротическое фэнтези', value: 'fantasy-erotika' }, + { label: 'Юмор', value: 'humor' }, + { label: 'Юмористическая фантастика', value: 'sf-humor' }, + { label: 'Юмористическое фэнтези', value: 'ironical-fantasy' }, + ], + type: FilterTypes.Picker, + }, + form: { + label: 'Форма произведения', + value: '', + options: [ + { label: 'Любой', value: '' }, + { label: 'Перевод', value: 'translation' }, + { label: 'Повесть', value: 'tale' }, + { label: 'Рассказ', value: 'story' }, + { label: 'Роман', value: 'novel' }, + { label: 'Сборник поэзии', value: 'poetry' }, + { label: 'Сборник рассказов', value: 'story-book' }, + ], + type: FilterTypes.Picker, + }, + state: { + label: 'Статус произведения', + value: '', + options: [ + { label: 'Любой статус', value: '' }, + { label: 'В процессе', value: 'in-progress' }, + { label: 'Завершено', value: 'finished' }, + ], + type: FilterTypes.Picker, + }, + series: { + label: 'Статус цикла', + value: '', + options: [ + { label: 'Не важно', value: '' }, + { label: 'Вне цикла', value: 'out' }, + { label: 'Цикл завершен', value: 'finished' }, + { label: 'Цикл не завершен', value: 'unfinished' }, + ], + type: FilterTypes.Picker, + }, + access: { + label: 'Тип доступа', + value: '', + options: [ + { label: 'Любой', value: '' }, + { label: 'Платный', value: 'paid' }, + { label: 'Бесплатный', value: 'free' }, + ], + type: FilterTypes.Picker, + }, + promo: { + label: 'Промо-фрагмент', + value: 'hide', + options: [ + { label: 'Скрывать', value: 'hide' }, + { label: 'Показывать', value: 'show' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new AuthorToday(); + +function decrypt( + encrypt: string, + encryptedKey: string, + userId: string | number = '', +) { + const key = encryptedKey.split('').reverse().join('') + '@_@' + userId; + const keyBytes = key.split('').map(char => char.charCodeAt(0)); + const keyLength = keyBytes.length; + let text = ''; + + for (let i = 0; i < encrypt.length; i++) { + text += String.fromCharCode( + encrypt.charCodeAt(i) ^ keyBytes[i % keyLength], + ); + } + + return text; +} + +type Result = { + isSuccessful: boolean; + isWarning: boolean; + messages: string | null; + data: Data; +}; + +type Data = { + text: string; +}; diff --git a/plugins/russian/bookriver.ts b/plugins/russian/bookriver.ts new file mode 100644 index 000000000..0a51a1e06 --- /dev/null +++ b/plugins/russian/bookriver.ts @@ -0,0 +1,299 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +const regex = + /<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/; + +class Bookriver implements Plugin.PluginBase { + id = 'bookriver'; + name = 'Bookriver'; + site = 'https://bookriver.ru'; + apiSite = 'https://api.bookriver.ru/api/v1/'; + version = '1.0.1'; + icon = 'src/ru/bookriver/icon.png'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/genre?page=' + pageNo + '&perPage=24&sortingType='; + url += showLatestNovels + ? 'last-update' + : filters?.sort?.value || 'bestseller'; + + if (filters?.genres?.value?.length) { + url += '&g=' + filters.genres.value.join(','); + } + + const result = await fetchApi(url).then(res => res.text()); + const novels: Plugin.NovelItem[] = []; + + const jsonRaw = result.match(regex); + if (jsonRaw instanceof Array && jsonRaw[1]) { + const json: response = JSON.parse(jsonRaw[1]); + + json.props.pageProps.state.pagesFilter?.genre?.books?.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.coverImages[0].url, + path: novel.slug, + }), + ); + } + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + }; + + const jsonRaw = result.match(regex); + if (jsonRaw instanceof Array && jsonRaw[1]) { + const book: BooksEntity = JSON.parse(jsonRaw[1]).props.pageProps.state + .book.bookPage; + + novel.name = book.name || ''; + novel.cover = book.coverImages[0].url; + novel.summary = book.annotation; + novel.author = book.author?.name; + + novel.status = + book?.statusComplete === 'writing' + ? NovelStatus.Ongoing + : NovelStatus.Completed; + + if (book.tags?.length) + novel.genres = book.tags?.map(tag => tag.name).join(','); + + if (book?.ebook?.chapters?.length) { + const chapters: Plugin.ChapterItem[] = []; + + book.ebook.chapters.forEach((chapter, chapterIndex) => { + if (chapter.available) { + chapters.push({ + name: chapter.name, + path: novelPath + '/' + chapter.chapterId, + releaseTime: dayjs( + chapter.firstPublishedAt || chapter.createdAt || undefined, + ).format('LLL'), + chapterNumber: chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + } + } + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = this.apiSite + 'books/chapter/text/'; + const { data }: responseChapter = await fetchApi( + url + chapterPath.split('/').pop(), + ).then(res => res.json()); + + let chapterText = data.content || 'Конец произведения'; + if (data?.audio?.available) { + chapterText += '\n' + data.audio.url; + } + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number | undefined = 1, + ): Promise<Plugin.NovelItem[]> { + const url = + this.apiSite + + 'search/autocomplete?keyword=' + + searchTerm + + '&page=' + + pageNo + + '&perPage=10'; + const { data }: responseSearch = await fetchApi(url).then(res => + res.json(), + ); + + const novels: Plugin.NovelItem[] = []; + data?.books?.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.coverImages[0].url, + path: novel.slug, + }), + ); + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/reader/') + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'bestseller', + options: [ + { label: 'Бестселлеры', value: 'bestseller' }, + { label: 'Дате добавления', value: 'newest' }, + { label: 'Дате обновления', value: 'last-update' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'жанры', + value: [], + options: [ + { label: 'Альтернативная история', value: 'alternativnaya-istoriya' }, + { label: 'Боевая фантастика', value: 'boevaya-fantastika' }, + { label: 'Боевое фэнтези', value: 'boevoe-fentezi' }, + { label: 'Бытовое фэнтези', value: 'bytovoe-fentezi' }, + { label: 'Героическая фантастика', value: 'geroicheskaya-fantastika' }, + { label: 'Героическое фэнтези', value: 'geroicheskoe-fentezi' }, + { label: 'Городское фэнтези', value: 'gorodskoe-fentezi' }, + { label: 'Детектив', value: 'detektiv' }, + { label: 'Детективная фантастика', value: 'detektivnaya-fantastika' }, + { label: 'Жёсткая эротика', value: 'zhyostkaya-erotika' }, + { label: 'Исторический детектив', value: 'istoricheskii-detektiv' }, + { + label: 'Исторический любовный роман', + value: 'istoricheskii-lyubovnyi-roman', + }, + { label: 'Историческое фэнтези', value: 'istoricheskoe-fentezi' }, + { label: 'Киберпанк', value: 'kiberpank' }, + { label: 'Классический детектив', value: 'klassicheskii-detektiv' }, + { label: 'Короткий любовный роман', value: 'korotkii-lyubovnyi-roman' }, + { label: 'Космическая фантастика', value: 'kosmicheskaya-fantastika' }, + { label: 'Криминальный детектив', value: 'kriminalnyi-detektiv' }, + { label: 'ЛитРПГ', value: 'litrpg' }, + { label: 'Любовная фантастика', value: 'lyubovnaya-fantastika' }, + { label: 'Любовное фэнтези', value: 'lyubovnoe-fentezi' }, + { label: 'Любовный роман', value: 'lyubovnyi-roman' }, + { label: 'Мистика', value: 'mistika' }, + { label: 'Молодежная проза', value: 'molodezhnaya-proza' }, + { label: 'Научная фантастика', value: 'nauchnaya-fantastika' }, + { + label: 'Остросюжетный любовный роман', + value: 'ostrosyuzhetnyi-lyubovnyi-roman', + }, + { label: 'Политический детектив', value: 'politicheskii-detektiv' }, + { label: 'Попаданцы', value: 'popadantsy' }, + { label: 'Постапокалипсис', value: 'postapokalipsis' }, + { + label: 'Приключенческое фэнтези', + value: 'priklyuchencheskoe-fentezi', + }, + { label: 'Романтическая эротика', value: 'romanticheskaya-erotika' }, + { label: 'С элементами эротики', value: 's-elementami-erotiki' }, + { label: 'Славянское фэнтези', value: 'slavyanskoe-fentezi' }, + { + label: 'Современный любовный роман', + value: 'sovremennyi-lyubovnyi-roman', + }, + { label: 'Социальная фантастика', value: 'sotsialnaya-fantastika' }, + { label: 'Тёмное фэнтези', value: 'temnoe-fentezi' }, + { label: 'Фантастика', value: 'fantastika' }, + { label: 'Фэнтези', value: 'fentezi' }, + { label: 'Шпионский детектив', value: 'shpionskii-detektiv' }, + { label: 'Эпическое фэнтези', value: 'epicheskoe-fentezi' }, + { label: 'Эротика', value: 'erotika' }, + { label: 'Эротическая фантастика', value: 'eroticheskaya-fantastika' }, + { label: 'Эротический фанфик', value: 'eroticheskii-fanfik' }, + { label: 'Эротическое фэнтези', value: 'eroticheskoe-fentezi' }, + { + label: 'Юмористический детектив', + value: 'yumoristicheskii-detektiv', + }, + { label: 'Юмористическое фэнтези', value: 'yumoristicheskoe-fentezi' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} +export default new Bookriver(); + +type response = { + props: { + pageProps: { + state: { + book?: { + bookPage: BooksEntity; + }; + pagesFilter?: { + genre: { + books: BooksEntity[]; + }; + }; + }; + }; + }; +}; + +type BooksEntity = { + name: string; + coverImages: { url: string }[]; + slug: string; + annotation?: string; + author?: { + name: string; + }; + tags?: { name: string }[]; + ebook?: { + chapters: Chapter[]; + }; + statusComplete: string; +}; + +type Chapter = { + name: string; + available: boolean; + firstPublishedAt?: Date | null; + createdAt?: Date | null; + chapterId: number | string; +}; + +type responseChapter = { + data: { + id: number; + bookId: number; + name: string; + content: string; + symbols: number; + number: number; + authorPages: number; + createdAt: string; + updatedAt: string; + firstPublishedAt: string; + status: string; + protected: boolean; + free: boolean; + audio?: { + available: boolean; + url: string; + duration: number; + }; + publicationScheduledFor?: null; + }; +}; + +type responseSearch = { + data: Data; + total: number; +}; +type Data = { + books?: BooksEntity[] | null; +}; diff --git a/plugins/russian/ficbook.ts b/plugins/russian/ficbook.ts new file mode 100644 index 000000000..e3430729b --- /dev/null +++ b/plugins/russian/ficbook.ts @@ -0,0 +1,2676 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import dayjs from 'dayjs'; + +class ficbook implements Plugin.PluginBase { + id = 'ficbook'; + name = 'ficbook'; + site = 'https://ficbook.net'; + imageSite = 'https://images.ficbook.net/fanfic-covers/'; + version = '1.0.1'; + icon = 'src/ru/ficbook/icon.png'; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + let url = this.site; + + if (filters?.directions?.value) { + url += '/popular-fanfics/' + filters.directions.value; + } else if (filters?.tags?.value) { + url += '/tags/' + filters.tags.value + '?p=' + pageNo; + } else { + url += '/' + (filters?.sort?.value || 'fanfiction') + '?p=' + pageNo; + } + + const result = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(result); + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('article.fanfic-inline').each((index, element) => { + const name = loadedCheerio(element).find('h3 > a').text().trim(); + let cover = loadedCheerio(element).find('picture > img').attr('src'); + const url = loadedCheerio(element).find('h3 > a').attr('href'); + + cover = cover + ? cover.replace(/covers\/m_|covers\/d_/g, 'covers/') + : defaultCover; + if (!name || !url) return; + + novels.push({ name, cover, path: url.replace(/\?.*/g, '') }); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(result); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: ( + loadedCheerio('h1[itemprop="headline"]').text() || + loadedCheerio('h1[itemprop="name"]').text() || + '' + ).trim(), + }; + novel.cover = loadedCheerio('meta[property="og:image"]').attr('content'); + novel.summary = loadedCheerio('div[itemprop="description"]').text().trim(); + novel.author = loadedCheerio('a[itemprop="author"]').text(); + + novel.status = + loadedCheerio( + 'div.fanfic-main-info > section:nth-child(3) > div:nth-child(3) > span:nth-child(2)', + ).text() === 'В процессе' + ? NovelStatus.Ongoing + : NovelStatus.Completed; + + const tags = loadedCheerio('div[class="tags"] > a') + .map((index, element) => loadedCheerio(element).text()) + .get(); + + if (tags.length) { + novel.genres = tags.join(','); + } + + if (!novel.cover || novel.cover?.includes('/design/')) { + novel.cover = defaultCover; + } else { + novel.cover = novel.cover.replace(/covers\/m_|covers\/d_/g, 'covers/'); + } + + const chapters: Plugin.ChapterItem[] = []; + + if (loadedCheerio('#content').length == 1) { + const name = loadedCheerio('.title-area > h2').text(); + const releaseTime = loadedCheerio('.part-date > span').attr('title'); + + if (name) chapters.push({ name, path: novelPath, releaseTime }); + } else { + loadedCheerio('li.part').each((chapterIndex, element) => { + const name = loadedCheerio(element).find('h3').text(); + const url = loadedCheerio(element).find('a:nth-child(1)').attr('href'); + if (!name || !url) return; + + const releaseTime = loadedCheerio(element) + .find('div > span') + .attr('title'); + chapters.push({ + name, + path: url.replace(this.site, ''), + releaseTime, + chapterNumber: chapterIndex + 1, + }); + }); + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(result); + let chapterText = ''; + + loadedCheerio('#content') + .html() + ?.split('\n') + ?.forEach(line => { + if (line.trim()) { + //blank line skip + if (line.includes('</p>')) { + //new paragraph + chapterText += line; + } else { + //plaintext + chapterText += '<p>' + line + '</p>'; + } + } + }); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number | undefined = 1, + ): Promise<Plugin.NovelItem[]> { + const formData = new FormData(); + formData.append('term', searchTerm); + formData.append('page', pageNo.toString()); + + const { data }: { data: Data } = await fetchApi( + this.site + '/search/fanfic', + { + method: 'POST', + body: formData, + }, + ).then(res => res.json()); + + const novels: Plugin.NovelItem[] = []; + + data?.data?.forEach(novel => { + const name = novel.title.trim(); + const path = '/readfic/' + novel.slug; + const cover = novel.cover ? this.imageSite + novel.cover : defaultCover; + + novels.push({ name, cover, path }); + }); + + return novels; + } + + parseDate = (dateString: string | undefined = '') => { + const months: Record<string, number> = { + января: 1, + февраля: 2, + марта: 3, + апреля: 4, + мая: 5, + июня: 6, + июля: 7, + августа: 8, + сентября: 9, + октября: 10, + ноября: 11, + декабря: 12, + }; + + const [day, month, year, , time] = dateString.split(' '); + if (day && month && year && months[month] && time) { + return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format( + 'LLL', + ); + } + return dateString || null; + }; + + filters = { + sort: { + label: 'Сортировка:', + value: 'fanfiction', + options: [ + { label: 'Горячие работы', value: 'fanfiction' }, + { label: 'Популярные ', value: 'popular-fanfics' }, + ], + type: FilterTypes.Picker, + }, + directions: { + label: 'Направление:', + value: '', + options: [ + { label: 'Все', value: '' }, + { label: 'Джен', value: 'gen' }, + { label: 'Гет', value: 'het' }, + { label: 'Слэш', value: 'slash-fics-3712917' }, + { label: 'Фемслэш', value: 'femslash-fanfics-9374932' }, + { label: 'Статьи', value: 'article' }, + { label: 'Смешанный', value: 'mixed' }, + { label: 'Другой', value: 'other' }, + ], + type: FilterTypes.Picker, + }, + tags: { + label: 'Тэги:', + value: '', + options: [ + { label: 'Все', value: '' }, + { label: '#1fic1week', value: '884' }, + { label: '#7daystowrite', value: '2961' }, + { label: '#Бинго_ТФ', value: '1458' }, + { label: '#миниатюры3days', value: '2118' }, + { label: '#Не_Тайный_Санта', value: '2760' }, + { label: '#Писательские фанты-блиц', value: '2613' }, + { label: '#cosmowrite', value: '2284' }, + { label: '#femKHRweek', value: '2658' }, + { label: '#FourShips', value: '2588' }, + { label: '#Goretober', value: '1861' }, + { label: '#InternationalVampireDay / #WorldDraculaDay', value: '2048' }, + { label: '#KattWeek', value: '2717' }, + { label: '#Kinktober', value: '1867' }, + { label: '#LotorWeek', value: '2657' }, + { label: '#MerMay', value: '1122' }, + { label: '#NaNoWriMo', value: '1852' }, + { label: '#nsfweruriweek', value: '2944' }, + { label: '#RCMiaChallenge', value: '2700' }, + { label: '#SheithMonth', value: '2432' }, + { label: '#SixDrabbles', value: '2293' }, + { label: '#Writober', value: '1831' }, + { label: '1900-е годы', value: '369' }, + { label: '1910-е годы', value: '213' }, + { label: '1920-е годы', value: '81' }, + { label: '1930-е годы', value: '82' }, + { label: '1940-е годы', value: '83' }, + { label: '1950-е годы', value: '84' }, + { label: '1960-е годы', value: '85' }, + { label: '1970-е годы', value: '86' }, + { label: '1980-е годы', value: '87' }, + { label: '1990-е годы', value: '88' }, + { label: '2000-е годы', value: '590' }, + { label: '2010-е годы', value: '2562' }, + { label: '2014 год', value: '2977' }, + { label: '2P', value: '1910' }, + { label: '9 мая', value: '1019' }, + { label: 'Аборт / Выкидыш', value: '306' }, + { label: 'Аватары', value: '2401' }, + { label: 'Авиатранспорт', value: '962' }, + { label: 'Авиация', value: '455' }, + { label: 'Аврорат', value: '1906' }, + { label: 'Австралия', value: '767' }, + { label: 'Австрия', value: '1024' }, + { label: 'Автомастерские', value: '788' }, + { label: 'Авторская пунктуация', value: '2886' }, + { label: 'Авторские неологизмы', value: '1123' }, + { label: 'Автоспорт', value: '856' }, + { label: 'Автостоп', value: '1429' }, + { label: 'Аддикции', value: '1230' }, + { label: 'Адреналиновая зависимость', value: '1480' }, + { label: 'Азартные игры', value: '1009' }, + { label: 'Азкабан', value: '1922' }, + { label: 'Аквакинез', value: '1380' }, + { label: 'Аквапарки', value: '789' }, + { label: 'Акростих', value: '911' }, + { label: 'Актеры', value: '408' }, + { label: 'Акустикофилия', value: '903' }, + { label: 'Алекситимия', value: '2020' }, + { label: 'Алкоголь', value: '201' }, + { label: 'Алкогольные игры', value: '1534' }, + { label: 'Аллергии', value: '746' }, + { label: 'Алхимия', value: '1174' }, + { label: 'Альбинизм', value: '1213' }, + { label: 'Альпинизм', value: '1448' }, + { label: 'Альтернативная мировая история', value: '442' }, + { label: 'Альтернативное размножение', value: '2751' }, + { label: 'Альтернативные судьбы', value: '2839' }, + { label: 'Амазонки', value: '1424' }, + { label: 'Амегакуре', value: '2571' }, + { label: 'Американский футбол', value: '808' }, + { label: 'Ампутация', value: '704' }, + { label: 'Амфибии', value: '2792' }, + { label: 'Анальгезия', value: '1254' }, + { label: 'Анальный оргазм', value: '999' }, + { label: 'Анальный секс', value: '215' }, + { label: 'Анархия', value: '1175' }, + { label: 'Анахронизмы', value: '780' }, + { label: 'Ангелы', value: '91' }, + { label: 'Ангелы-хранители', value: '1044' }, + { label: 'Ангст', value: '1665' }, + { label: 'Андрогинная внешность', value: '2602' }, + { label: 'Андроиды', value: '155' }, + { label: 'Анимагия', value: '2519' }, + { label: 'Анимализм', value: '1443' }, + { label: 'Аномальные зоны', value: '822' }, + { label: 'Анонимный секс', value: '112' }, + { label: 'Анрождение', value: '1246' }, + { label: 'Антарктида', value: '2205' }, + { label: 'Анти-Сью (Анти-Стью)', value: '1150' }, + { label: 'Антигерои', value: '418' }, + { label: 'Антизлодеи', value: '1833' }, + { label: 'Антикварные магазины', value: '1496' }, + { label: 'Антисоциальное расстройство личности', value: '734' }, + { label: 'Антиутопия', value: '1700' }, + { label: 'Античность', value: '237' }, + { label: 'Апатия', value: '1850' }, + { label: 'Аптеки', value: '1240' }, + { label: 'Аргентина', value: '1146' }, + { label: 'Арены', value: '940' }, + { label: 'Аристократия', value: '16' }, + { label: 'Арктика', value: '2426' }, + { label: 'Армия', value: '22' }, + { label: 'Аромантичные персонажи', value: '339' }, + { label: 'Артефакты', value: '924' }, + { label: 'Археологи', value: '405' }, + { label: 'Архивы', value: '2594' }, + { label: 'Архитекторы', value: '1011' }, + { label: 'Асексофобия', value: '1120' }, + { label: 'Асексуальные персонажи', value: '168' }, + { label: 'Аскетизм', value: '606' }, + { label: 'Асоциальность', value: '2019' }, + { label: 'Ассистенты', value: '2641' }, + { label: 'Астма', value: '1121' }, + { label: 'Астральное проецирование', value: '2530' }, + { label: 'Астральные миры', value: '1379' }, + { label: 'Астрология', value: '476' }, + { label: 'Астронавты', value: '2220' }, + { label: 'Астрономия', value: '1372' }, + { label: 'Асфиксия', value: '104' }, + { label: 'Атеисты', value: '669' }, + { label: 'Атлантида', value: '1144' }, + { label: 'Атмокинез', value: '1964' }, + { label: 'Атмосферная зарисовка', value: '2802' }, + { label: 'Атомные электростанции', value: '2192' }, + { label: 'Аудиокинез', value: '2849' }, + { label: 'Аукционы', value: '607' }, + { label: 'Аутоассасинофилия', value: '2025' }, + { label: 'Аутоканнибализм', value: '2593' }, + { label: 'Аутофелляция / Аутокуннилингус', value: '610' }, + { label: 'Африка', value: '791' }, + { label: 'Афродизиаки', value: '103' }, + { label: 'Аффект', value: '938' }, + { label: 'Аэрокинез', value: '2850' }, + { label: 'Байкеры', value: '522' }, + { label: 'Байронические герои', value: '2900' }, + { label: 'Балет', value: '404' }, + { label: 'Баллада', value: '2318' }, + { label: 'Банки / Биржи', value: '937' }, + { label: 'Банши', value: '250' }, + { label: 'Барти Крауч-младший — не Пожиратель Смерти', value: '2795' }, + { label: 'Бары', value: '142' }, + { label: 'Баскетбол', value: '829' }, + { label: 'Баски', value: '1763' }, + { label: 'Бастарды', value: '603' }, + { label: 'Батохаки', value: '2830' }, + { label: 'Бедность', value: '979' }, + { label: 'Без Волдеморта', value: '2160' }, + { label: 'Без диалогов (стилизация)', value: '2885' }, + { label: 'Без золотого трио', value: '2648' }, + { label: 'Без Избранного (Гарри Поттер)', value: '2762' }, + { label: 'Без канонических персонажей', value: '2694' }, + { label: 'Без Таноса', value: '2381' }, + { label: 'БезГражданки', value: '2418' }, + { label: 'Бездомные', value: '916' }, + { label: 'Безответственность', value: '2152' }, + { label: 'Безумные ученые', value: '2559' }, + { label: 'Безэмоциональность', value: '1488' }, + { label: 'Бейсбол', value: '1489' }, + { label: 'Беларусь', value: '1030' }, + { label: 'Белый день', value: '683' }, + { label: 'Белый стих', value: '1998' }, + { label: 'Бельгия', value: '1039' }, + { label: 'Беременность', value: '1712' }, + { label: 'Бермудский треугольник', value: '1158' }, + { label: 'Берсерки', value: '566' }, + { label: 'Бесконтактный секс', value: '2214' }, + { label: 'Бесплодие', value: '719' }, + { label: 'Беспокойство', value: '2888' }, + { label: 'Беспризорники', value: '604' }, + { label: 'Беспричудный Изуку', value: '2422' }, + { label: 'Бессмертие', value: '219' }, + { label: 'Бессмертный конкурс 2023', value: '3016' }, + { label: 'Бестелесность', value: '2607' }, + { label: 'Би-персонажи', value: '300' }, + { label: 'Библейские темы и мотивы', value: '1468' }, + { label: 'Библиотекари', value: '2631' }, + { label: 'Библиотеки', value: '310' }, + { label: 'Бизнесмены / Бизнесвумен', value: '573' }, + { label: 'Биологическое оружие', value: '1188' }, + { label: 'Биопанк', value: '281' }, + { label: 'Биороботы', value: '2256' }, + { label: 'Биполярное расстройство', value: '690' }, + { label: 'Битва за Хогвартс', value: '2774' }, + { label: 'Битва Историй', value: '2960' }, + { label: 'Благородные разбойники', value: '952' }, + { label: 'Благотворительность', value: '613' }, + { label: 'Бладплей', value: '109' }, + { label: 'Ближний Восток', value: '1041' }, + { label: 'Близкие враги', value: '721' }, + { label: 'Близнецы', value: '295' }, + { label: 'Блиц-свидания', value: '2511' }, + { label: 'Блогеры', value: '1471' }, + { label: 'Блокадный Ленинград', value: '1111' }, + { label: 'Блэкигуд', value: '2905' }, + { label: 'Богачи', value: '1549' }, + { label: 'Боги / Божественные сущности', value: '149' }, + { label: 'Боди-арт', value: '2260' }, + { label: 'Бодипозитив', value: '1153' }, + { label: 'Боевая пара', value: '1919' }, + { label: 'Боевые искусства', value: '918' }, + { label: 'Бои без правил', value: '1390' }, + { label: 'Бокс', value: '765' }, + { label: 'Болгария', value: '1786' }, + { label: 'Болезнь Альцгеймера', value: '2533' }, + { label: 'Боль', value: '1803' }, + { label: 'Больницы', value: '20' }, + { label: 'Большая Игра профессора Дамблдора', value: '2890' }, + { label: 'Бордели', value: '120' }, + { label: 'Борьба за власть', value: '2300' }, + { label: 'Борьба за отношения', value: '1251' }, + { label: 'Борьба за справедливость', value: '2335' }, + { label: 'Боязнь боли', value: '2441' }, + { label: 'Боязнь воды', value: '1084' }, + { label: 'Боязнь врачей', value: '2063' }, + { label: 'Боязнь высоты', value: '1085' }, + { label: 'Боязнь грозы', value: '1124' }, + { label: 'Боязнь громких звуков', value: '1090' }, + { label: 'Боязнь грязи', value: '1277' }, + { label: 'Боязнь замкнутых пространств', value: '821' }, + { label: 'Боязнь зеркал', value: '2739' }, + { label: 'Боязнь клоунов', value: '2970' }, + { label: 'Боязнь крови', value: '1312' }, + { label: 'Боязнь людей', value: '1082' }, + { label: 'Боязнь мужчин', value: '1264' }, + { label: 'Боязнь насекомых / Боязнь пауков', value: '1119' }, + { label: 'Боязнь неудачи', value: '2713' }, + { label: 'Боязнь огня', value: '1258' }, + { label: 'Боязнь одиночества', value: '1491' }, + { label: 'Боязнь открытых пространств', value: '965' }, + { label: 'Боязнь привязанности', value: '1343' }, + { label: 'Боязнь призраков', value: '2770' }, + { label: 'Боязнь прикосновений', value: '823' }, + { label: 'Боязнь сексуальных домогательств', value: '2319' }, + { label: 'Боязнь смерти', value: '1515' }, + { label: 'Боязнь сна', value: '2262' }, + { label: 'Боязнь темноты', value: '1494' }, + { label: 'Боязнь тишины', value: '2233' }, + { label: 'Бразилия', value: '833' }, + { label: 'Брак по договоренности', value: '35' }, + { label: 'Брак по расчету', value: '303' }, + { label: 'БРД', value: '2907' }, + { label: 'Бродячие артисты', value: '2661' }, + { label: 'Бродячие певцы', value: '1497' }, + { label: 'Броманс', value: '171' }, + { label: 'Будущее', value: '227' }, + { label: 'Буллёзный эпидермолиз', value: '2725' }, + { label: 'Буллинг', value: '436' }, + { label: 'Бухгалтеры', value: '2509' }, + { label: 'Буч и фэм', value: '486' }, + { label: 'Бывшие', value: '311' }, + { label: 'Бывшие враги', value: '2812' }, + { label: 'Бывшие серийные убийцы', value: '2498' }, + { label: 'Бытовое фэнтези', value: '2702' }, + { label: 'В одном теле', value: '1425' }, + { label: 'В поисках отношений', value: '1576' }, + { label: 'В. А. О. Н.', value: '2377' }, + { label: 'Вагинальный секс', value: '395' }, + { label: 'Ваканда', value: '2668' }, + { label: 'Валькирии', value: '557' }, + { label: 'Вампиры', value: '27' }, + { label: 'Василиски', value: '863' }, + { label: 'Вдовство', value: '2030' }, + { label: 'Вебкам-модели', value: '2299' }, + { label: 'Вегетарианцы / Веганы', value: '577' }, + { label: 'Ведьмы / Колдуны', value: '33' }, + { label: 'Великая депрессия', value: '2224' }, + { label: 'Великая французская революция', value: '2000' }, + { label: 'Великобритания', value: '673' }, + { label: 'Великое Княжество Литовское', value: '2587' }, + { label: 'Великолепный мерзавец', value: '1244' }, + { label: 'Велоспорт', value: '2978' }, + { label: 'Венгрия', value: '1432' }, + { label: 'Вендиго', value: '251' }, + { label: 'Верность', value: '459' }, + { label: 'Вертикальный инцест', value: '337' }, + { label: 'Вестерн', value: '2329' }, + { label: 'Ветераны', value: '555' }, + { label: 'Ветеринары', value: '629' }, + { label: 'Вечеринки', value: '854' }, + { label: 'Вечная молодость', value: '2405' }, + { label: 'Вещие сны', value: '1993' }, + { label: 'Взаимопонимание', value: '2887' }, + { label: 'Взросление', value: '909' }, + { label: 'Вигиланты', value: '1514' }, + { label: 'Видеоигры', value: '2508' }, + { label: 'Визажисты / Гримеры', value: '1801' }, + { label: 'Викинги', value: '676' }, + { label: 'Виктимблейминг', value: '331' }, + { label: 'Викторианская эпоха', value: '469' }, + { label: 'Виртуальная реальность', value: '803' }, + { label: 'Виртуальный секс', value: '268' }, + { label: 'Витилиго', value: '1167' }, + { label: 'Вице-королевство Новая Испания', value: '2603' }, + { label: 'ВИЧ / СПИД', value: '542' }, + { label: 'Влюбленность', value: '1303' }, + { label: 'Вне закона', value: '1089' }, + { label: 'Внутренний сексизм', value: '2155' }, + { label: 'Внутренняя гомофобия', value: '2154' }, + { label: 'Внутренняя трансфобия', value: '2716' }, + { label: 'Водители / Шоферы', value: '879' }, + { label: 'Военные', value: '313' }, + { label: 'Военные городки', value: '2095' }, + { label: 'Военные преступления', value: '2356' }, + { label: 'Возвращение', value: '1522' }, + { label: 'Возвращение Артура', value: '2570' }, + { label: 'Воздержание', value: '921' }, + { label: 'Воздушная атлетика', value: '2811' }, + { label: 'Война', value: '232' }, + { label: 'Война в Афганистане', value: '1050' }, + { label: 'Война во Вьетнаме', value: '1332' }, + { label: 'Война за независимость США', value: '2065' }, + { label: 'Война миров', value: '1222' }, + { label: 'Волдигуд', value: '1874' }, + { label: 'Волейбол', value: '1095' }, + { label: 'Волк в овечьей шкуре', value: '2228' }, + { label: 'Волонтеры', value: '900' }, + { label: 'Волшебники / Волшебницы', value: '93' }, + { label: 'Воображаемые друзья', value: '738' }, + { label: 'Ворарефилия', value: '157' }, + { label: 'Воры', value: '320' }, + { label: 'Воскрешение', value: '256' }, + { label: 'Воспоминания', value: '1118' }, + { label: 'Воссоединение', value: '498' }, + { label: 'Восстание машин', value: '817' }, + { label: 'Восточное побережье', value: '770' }, + { label: 'Восьмое марта', value: '493' }, + { label: 'Вражда', value: '681' }, + { label: 'Вражда семей', value: '2867' }, + { label: 'Врачи', value: '374' }, + { label: 'Времена Мародеров', value: '1872' }, + { label: 'Времена Основателей', value: '1891' }, + { label: 'Временная смерть персонажа', value: '439' }, + { label: 'Временное бессмертие', value: '2755' }, + { label: 'Временное воскрешение персонажей', value: '2649' }, + { label: 'Временные петли', value: '147' }, + { label: 'Временные превращения', value: '927' }, + { label: 'Врожденные заболевания', value: '1781' }, + { label: 'Всадники Апокалипсиса', value: '1326' }, + { label: 'Все живы / Никто не умер', value: '535' }, + { label: 'Все за Одного — отец Изуку', value: '2421' }, + { label: 'Всезнающий рассказчик', value: '1368' }, + { label: 'Вселение в тело', value: '1181' }, + { label: 'Вторая мировая', value: '488' }, + { label: 'Вторжение пришельцев', value: '755' }, + { label: 'Вторичный стыд', value: '1393' }, + { label: 'Второй шанс', value: '1799' }, + { label: 'Второстепенные оригинальные персонажи', value: '1536' }, + { label: 'Вуайеризм', value: '174' }, + { label: 'Вуду', value: '678' }, + { label: 'Выбор', value: '2808' }, + { label: 'Выгорание', value: '2914' }, + { label: 'Выживание', value: '216' }, + { label: 'Вымирающий вид', value: '2373' }, + { label: 'Вымышленная анатомия', value: '1317' }, + { label: 'Вымышленная география', value: '1294' }, + { label: 'Вымышленная религия', value: '2062' }, + { label: 'Вымышленная физика', value: '2132' }, + { label: 'Вымышленные виды спорта', value: '1777' }, + { label: 'Вымышленные заболевания', value: '1295' }, + { label: 'Вымышленные науки', value: '1333' }, + { label: 'Вымышленные праздники', value: '1769' }, + { label: 'Вымышленные профессии', value: '1391' }, + { label: 'Вымышленные существа', value: '1685' }, + { label: 'Вымышленные языки', value: '536' }, + { label: 'Выпускные', value: '717' }, + { label: 'Высшее общество', value: '1093' }, + { label: 'Высшие учебные заведения', value: '97' }, + { label: 'Выученная беспомощность', value: '2772' }, + { label: 'Выхаживание', value: '511' }, + { label: 'Выход из нездоровых отношений', value: '2891' }, + { label: 'Гадалки / Ясновидящие', value: '2662' }, + { label: 'Гадкий утенок', value: '1225' }, + { label: 'Газлайтинг', value: '1418' }, + { label: 'Галлюцинации / Иллюзии', value: '365' }, + { label: 'Гаремник', value: '50' }, + { label: 'Гаремы', value: '115' }, + { label: 'Гарпии', value: '1482' }, + { label: 'Гарри Поттер и Драко Малфой — друзья', value: '1923' }, + { label: 'Гастроли', value: '2487' }, + { label: 'Гедонизм', value: '736' }, + { label: 'Гей-клубы', value: '2482' }, + { label: 'Геймеры', value: '356' }, + { label: 'Геймлит', value: '23' }, + { label: 'Гейши / Кисэн', value: '332' }, + { label: 'Гемофилия', value: '1341' }, + { label: 'Гендерная дисфория', value: '431' }, + { label: 'Гендерная интрига', value: '55' }, + { label: 'Гендерное неравенство', value: '2568' }, + { label: 'Гендерный нонконформизм', value: '1417' }, + { label: 'Гендерсвап', value: '1691' }, + { label: 'Генетика / Эволюция', value: '2131' }, + { label: 'Гении', value: '445' }, + { label: 'Генитальные пытки', value: '2104' }, + { label: 'Геноцид', value: '720' }, + { label: 'Геокинез', value: '2889' }, + { label: 'Геологи / Географы', value: '934' }, + { label: 'Германия', value: '688' }, + { label: 'Германия — не Священная Римская Империя', value: '2470' }, + { label: 'Герой поневоле', value: '2145' }, + { label: 'Геронтофилия', value: '162' }, + { label: 'Гетерохромия', value: '1210' }, + { label: 'Гетто', value: '1783' }, + { label: 'Гибристофилия', value: '1843' }, + { label: 'Гиганты', value: '735' }, + { label: 'Гидра трэш-пати', value: '2804' }, + { label: 'Гики', value: '482' }, + { label: 'Гильдии', value: '957' }, + { label: 'Гимнастика', value: '1974' }, + { label: 'Гиперакузия', value: '2866' }, + { label: 'Гиперсексуальность', value: '945' }, + { label: 'Гипноз', value: '700' }, + { label: 'Гладиаторы', value: '677' }, + { label: 'Глобальные катастрофы', value: '525' }, + { label: 'Глухота', value: '595' }, + { label: 'Гнев', value: '2806' }, + { label: 'Гнездование', value: '1452' }, + { label: 'Гномы', value: '138' }, + { label: 'Гоблины', value: '188' }, + { label: 'Големы', value: '1208' }, + { label: 'Голод', value: '868' }, + { label: 'Голодные Фэндомные Игры', value: '2895' }, + { label: 'Гомофобия', value: '428' }, + { label: 'Гомункулы', value: '1366' }, + { label: 'Горе / Утрата', value: '1113' }, + { label: 'Горизонтальный инцест', value: '338' }, + { label: 'Городские легенды', value: '1832' }, + { label: 'Горы', value: '702' }, + { label: 'Горячие источники', value: '2813' }, + { label: 'Гостевой брак', value: '1354' }, + { label: 'Гостиницы', value: '384' }, + { label: 'Готический роман', value: '963' }, + { label: 'Готы', value: '575' }, + { label: 'Гравиокинез', value: '1985' }, + { label: 'Гражданская война', value: '370' }, + { label: 'Гражданская война в России', value: '1282' }, + { label: 'Гражданская война в США', value: '1018' }, + { label: 'Графичные описания', value: '2854' }, + { label: 'Грейнджергад', value: '1892' }, + { label: 'Грейромантичные персонажи', value: '707' }, + { label: 'Гремлины', value: '189' }, + { label: 'Греция', value: '1040' }, + { label: 'Грифоны', value: '335' }, + { label: 'Громкий секс', value: '1184' }, + { label: 'Грубый секс', value: '121' }, + { label: 'Грудное кормление', value: '296' }, + { label: 'Групи', value: '2925' }, + { label: 'Групповое изнасилование', value: '2203' }, + { label: 'Групповой секс', value: '1654' }, + { label: 'Грязный реализм', value: '634' }, + { label: 'Гувернантки', value: '1179' }, + { label: 'Гули', value: '367' }, + { label: 'Гэвин Рид — андроид', value: '2692' }, + { label: 'Даб-кон', value: '57' }, + { label: 'Даби — не Тойя Тодороки', value: '2821' }, + { label: 'Даби — Тойя Тодороки', value: '2423' }, + { label: 'Дадзава', value: '2915' }, + { label: 'Дадмайт', value: '2917' }, + { label: 'Дайверы', value: '939' }, + { label: 'Дайвинг', value: '2823' }, + { label: 'Дама в беде', value: '1259' }, + { label: 'Дамбигад', value: '1863' }, + { label: 'Дамбигуд', value: '1873' }, + { label: 'Дампиры', value: '2860' }, + { label: 'Дандэрэ', value: '1364' }, + { label: 'Дарк', value: '1678' }, + { label: 'Даркнет', value: '1287' }, + { label: 'Дачи', value: '930' }, + { label: 'Двойники', value: '305' }, + { label: 'Двойное проникновение', value: '107' }, + { label: 'Двойной кноттинг', value: '2619' }, + { label: 'Двойные агенты', value: '441' }, + { label: 'Дворцовые интриги', value: '2075' }, + { label: 'Девиантное поведение', value: '1814' }, + { label: 'Девичники / Мальчишники', value: '1191' }, + { label: 'Девочки из Эквестрии', value: '2912' }, + { label: 'Дежавю', value: '2049' }, + { label: 'Декаданс', value: '2353' }, + { label: 'Деконструкция', value: '1322' }, + { label: 'Деми-персонажи', value: '723' }, + { label: 'Демиурги', value: '819' }, + { label: 'Демонология', value: '2485' }, + { label: 'Демоны', value: '73' }, + { label: 'Демоны-хранители', value: '2045' }, + { label: 'Дендрофилия', value: '491' }, + { label: 'День благодарения', value: '754' }, + { label: 'День знаний', value: '1075' }, + { label: 'День матери', value: '2979' }, + { label: 'День мертвых', value: '1074' }, + { label: 'День независимости США', value: '1008' }, + { label: 'День рождения', value: '298' }, + { label: 'День святого Валентина', value: '204' }, + { label: 'День святого Патрика', value: '1129' }, + { label: 'День смерти', value: '2251' }, + { label: 'День учителя', value: '1076' }, + { label: 'Деперсонализация', value: '1197' }, + { label: 'Депрессия', value: '354' }, + { label: 'Депривация сна', value: '551' }, + { label: 'Дереализация', value: '1198' }, + { label: 'Деревни', value: '364' }, + { label: 'Дерматозойный бред', value: '2779' }, + { label: 'Деспоты / Тираны', value: '1345' }, + { label: 'Детектив', value: '1672' }, + { label: 'Дети', value: '217' }, + { label: 'Детоненавистничество', value: '1836' }, + { label: 'Детская влюбленность', value: '186' }, + { label: 'Детские дома', value: '355' }, + { label: 'Детские лагеря', value: '222' }, + { label: 'Детские сады', value: '567' }, + { label: 'Дефекты зрения', value: '2639' }, + { label: 'Дефекты речи', value: '737' }, + { label: 'Джинны / Пери', value: '456' }, + { label: 'Джон Ватсон — преступник', value: '1932' }, + { label: 'Джуманджи — не игровой мир', value: '2799' }, + { label: 'Диабет', value: '820' }, + { label: 'Диалоги (стилизация)', value: '1139' }, + { label: 'Дизайнеры', value: '1875' }, + { label: 'Дизельпанк', value: '294' }, + { label: 'Дикие животные', value: '622' }, + { label: 'Дикий Запад', value: '293' }, + { label: 'Дилетанты', value: '2443' }, + { label: 'Динозавры', value: '670' }, + { label: 'Дипломатия', value: '1790' }, + { label: 'Дисбаланс власти', value: '732' }, + { label: 'Дискриминация', value: '2230' }, + { label: 'Дискриминация по внешности', value: '1414' }, + { label: 'Дискриминация по возрасту', value: '2843' }, + { label: 'Дислексия', value: '860' }, + { label: 'Дисморфофобия', value: '1108' }, + { label: 'Диссоциативное расстройство идентичности', value: '2127' }, + { label: 'Дистанционное обучение', value: '2592' }, + { label: 'Дисфункциональные семьи', value: '602' }, + { label: 'Дневники (стилизация)', value: '214' }, + { label: 'Добрый Сальери', value: '2522' }, + { label: 'Доверие', value: '1117' }, + { label: 'Договоры / Сделки', value: '2137' }, + { label: 'Документы / Отчеты (стилизация)', value: '752' }, + { label: 'Долголетие', value: '2130' }, + { label: 'Домашнее насилие', value: '42' }, + { label: 'Домашние животные', value: '517' }, + { label: 'Домашний арест', value: '2526' }, + { label: 'Домашний ремонт', value: '867' }, + { label: 'Домовые', value: '1423' }, + { label: 'Допросы', value: '2654' }, + { label: 'Дорожное приключение', value: '47' }, + { label: 'Драббл', value: '2935' }, + { label: 'Драки', value: '1367' }, + { label: 'Драконы', value: '43' }, + { label: 'Драма', value: '1668' }, + { label: 'Драуги', value: '2264' }, + { label: 'Древний Восток', value: '1940' }, + { label: 'Древний Египет', value: '474' }, + { label: 'Древний Китай', value: '1438' }, + { label: 'Древняя Русь', value: '466' }, + { label: 'Дремлющее зло', value: '2719' }, + { label: 'Дремлющие способности', value: '2084' }, + { label: 'Другая магическая школа', value: '1864' }, + { label: 'Другие планеты', value: '1415' }, + { label: 'Другой избранный', value: '2054' }, + { label: 'Другой факультет', value: '1866' }, + { label: 'Дружба', value: '1701' }, + { label: 'Дружба втайне', value: '2303' }, + { label: 'Дружба по расчету', value: '2331' }, + { label: 'Друзья детства', value: '343' }, + { label: 'Друзья по переписке', value: '1466' }, + { label: 'Друзья поневоле', value: '627' }, + { label: 'Друзья с привилегиями', value: '113' }, + { label: 'Друиды', value: '1199' }, + { label: 'Дрэг (субкультура)', value: '1248' }, + { label: 'Дурмстранг', value: '2389' }, + { label: 'Дурслигуд', value: '2058' }, + { label: 'Духи природы', value: '503' }, + { label: 'Духовенство', value: '478' }, + { label: 'Духовная связь', value: '2090' }, + { label: 'Духокинез', value: '2500' }, + { label: 'Дуэли', value: '446' }, + { label: 'Дьяволы', value: '2369' }, + { label: 'Дэдди-кинк', value: '165' }, + { label: 'Еда / Кулинария', value: '1195' }, + { label: 'Единороги', value: '373' }, + { label: 'Ёкаи', value: '416' }, + { label: 'Естественные враги', value: '728' }, + { label: 'Жамевю', value: '2050' }, + { label: 'Жаргон', value: '1016' }, + { label: 'Ждун Фест', value: '2936' }, + { label: 'Железнодорожники', value: '2680' }, + { label: 'Железные дороги', value: '811' }, + { label: 'Женская дружба', value: '2156' }, + { label: 'Жертвы обстоятельств', value: '1475' }, + { label: 'Жестокое обращение с животными', value: '722' }, + { label: 'Жестокость', value: '1710' }, + { label: 'Живой сосуд', value: '2072' }, + { label: 'Животные-компаньоны', value: '2371' }, + { label: 'Живые машины', value: '961' }, + { label: 'ЖиП', value: '143' }, + { label: 'Журналисты', value: '438' }, + { label: 'Заболевания', value: '1268' }, + { label: 'Заболевания крови', value: '2254' }, + { label: 'Заболевания сердца', value: '1767' }, + { label: 'Забота / Поддержка', value: '1302' }, + { label: 'Заброшенные города', value: '1276' }, + { label: 'Заброшенные здания', value: '1483' }, + { label: 'Забытый герой', value: '2337' }, + { label: 'Завещание', value: '1262' }, + { label: 'Зависимое расстройство личности', value: '2349' }, + { label: 'Зависть', value: '990' }, + { label: 'Завоевание', value: '978' }, + { label: 'Загробный мир', value: '275' }, + { label: 'Закавказье', value: '2819' }, + { label: 'Заклятые друзья', value: '1316' }, + { label: 'Закрытые города', value: '1481' }, + { label: 'Закрытые учебные заведения', value: '98' }, + { label: 'Закрытый детектив', value: '2616' }, + { label: 'Заложники', value: '845' }, + { label: 'Замки', value: '654' }, + { label: 'Замкнутое пространство', value: '2306' }, + { label: 'Замкнутый мир', value: '874' }, + { label: 'Занавесочная история', value: '1688' }, + { label: 'Запахи', value: '1385' }, + { label: 'Заповедники', value: '618' }, + { label: 'Запредельно одаренный персонаж', value: '1241' }, + { label: 'Запрет на магию', value: '2663' }, + { label: 'Запретные отношения', value: '362' }, + { label: 'Засосы / Укусы', value: '913' }, + { label: 'Застенчивость', value: '2807' }, + { label: 'Затворники', value: '1059' }, + { label: 'Затерянные миры', value: '2448' }, + { label: 'Защита любимого', value: '2492' }, + { label: 'Защитники природы', value: '628' }, + { label: 'Защищенный секс', value: '636' }, + { label: 'Звездные слезы', value: '2971' }, + { label: 'Здоровые механизмы преодоления', value: '2147' }, + { label: 'Здоровые отношения', value: '1853' }, + { label: 'Зеркала', value: '724' }, + { label: 'Зимняя Фандомная Битва', value: '254' }, + { label: 'Злобные бывшие', value: '518' }, + { label: 'Зловещая находка', value: '2275' }, + { label: 'Знаки зодиака', value: '643' }, + { label: 'Знаменитости', value: '1360' }, + { label: 'Золотая клетка', value: '480' }, + { label: 'Золотая молодежь', value: '796' }, + { label: 'Золотая Орда', value: '1143' }, + { label: 'Золушка', value: '1226' }, + { label: 'Зомби', value: '31' }, + { label: 'Зоопарки', value: '792' }, + { label: 'Зоофилия', value: '1662' }, + { label: 'Зоофобии', value: '2715' }, + { label: 'ЗППП', value: '630' }, + { label: 'ЗПР', value: '2259' }, + { label: 'Зрелые персонажи', value: '851' }, + { label: 'Иван Купала', value: '1068' }, + { label: 'Игровое расстройство', value: '949' }, + { label: 'Игрушки', value: '1346' }, + { label: 'Игры на выживание', value: '554' }, + { label: 'Игры на раздевание', value: '1535' }, + { label: 'Игры с сосками', value: '423' }, + { label: 'Игры с температурой', value: '230' }, + { label: 'Идеализация', value: '1540' }, + { label: 'Иерархический строй', value: '652' }, + { label: 'Избранные', value: '847' }, + { label: 'Избыточный физиологизм', value: '2334' }, + { label: 'Изгнанники', value: '871' }, + { label: 'Измена', value: '62' }, + { label: 'Измененное состояние сознания', value: '2242' }, + { label: 'Изменились за лето', value: '1908' }, + { label: 'Изнасилование', value: '1650' }, + { label: 'Изоляция', value: '1296' }, + { label: 'Израиль', value: '1094' }, + { label: 'Изуку Мидория — злодей', value: '2472' }, + { label: 'Иллюзионисты', value: '532' }, + { label: 'Имейджин', value: '838' }, + { label: 'Импотенция', value: '632' }, + { label: 'Импринтинг', value: '635' }, + { label: 'Импрореал', value: '2955' }, + { label: 'Импрофест', value: '2947' }, + { label: 'Имя вместо цвета', value: '2919' }, + { label: 'Инвалидность', value: '197' }, + { label: 'Индия', value: '1002' }, + { label: 'Инквизиция', value: '1130' }, + { label: 'Инопланетяне', value: '58' }, + { label: 'Инсектоиды / Арахноиды', value: '953' }, + { label: 'Инсектофилия / Арахнофилия', value: '664' }, + { label: 'Инсценированная смерть персонажа', value: '440' }, + { label: 'Интерактивная работа', value: '419' }, + { label: 'Интервью (стилизация)', value: '2446' }, + { label: 'Интернет', value: '1286' }, + { label: 'Интерсекс-персонажи', value: '299' }, + { label: 'Интрамаммарный секс', value: '840' }, + { label: 'Инфантильность', value: '1300' }, + { label: 'Инцест', value: '1651' }, + { label: 'Ипохондрия', value: '2178' }, + { label: 'Ирландия', value: '1035' }, + { label: 'Иронический детектив', value: '2638' }, + { label: 'Искра Стайлз Стилински', value: '1975' }, + { label: 'Искупление', value: '877' }, + { label: 'Искусственно вызванные чувства', value: '626' }, + { label: 'Искусственные интеллекты', value: '150' }, + { label: 'Искусственные существа', value: '2277' }, + { label: 'Искусство', value: '2257' }, + { label: 'Искусствоведы', value: '2873' }, + { label: 'Искушение', value: '776' }, + { label: 'Испания', value: '1020' }, + { label: 'Исповеди', value: '1342' }, + { label: 'Исправительные лагеря', value: '2439' }, + { label: 'Истинные', value: '1855' }, + { label: 'Историческая Хеталия', value: '1865' }, + { label: 'Исторические эпохи', value: '1698' }, + { label: 'Исторический Кантриболс', value: '2913' }, + { label: 'Историческое допущение', value: '2681' }, + { label: 'Исцеление', value: '802' }, + { label: 'Италия', value: '1003' }, + { label: 'Ихтиоморфы', value: '972' }, + { label: 'Йога', value: '483' }, + { label: 'Йоль', value: '2143' }, + { label: 'Кабаре', value: '457' }, + { label: 'Кабэ-дон', value: '2626' }, + { label: 'Казахстан', value: '1260' }, + { label: 'Казино', value: '620' }, + { label: 'Казнь', value: '548' }, + { label: 'Как ориджинал', value: '1032' }, + { label: 'Каламбуры', value: '881' }, + { label: 'Каминг-аут', value: '559' }, + { label: 'Канада', value: '695' }, + { label: 'Каникулы', value: '805' }, + { label: 'Каннибализм', value: '1706' }, + { label: 'Канонная смерть персонажа', value: '564' }, + { label: 'Капитан Гидра', value: '1928' }, + { label: 'Карантинные зоны', value: '1290' }, + { label: 'Карантины', value: '2734' }, + { label: 'Караоке', value: '886' }, + { label: 'Карательная психиатрия', value: '730' }, + { label: 'Карибский бассейн', value: '1172' }, + { label: 'Карма', value: '647' }, + { label: 'Карьеризм', value: '956' }, + { label: 'Каскадеры', value: '2267' }, + { label: 'Катарсис', value: '824' }, + { label: 'Кафе / Кофейни / Чайные', value: '13' }, + { label: 'Квай-Гад', value: '2894' }, + { label: 'Квантовая физика', value: '2744' }, + { label: 'Квисины', value: '2722' }, + { label: 'Кемономими', value: '997' }, + { label: 'Кемпинг', value: '182' }, + { label: 'Кентавры', value: '64' }, + { label: 'Кибербуллинг', value: '1858' }, + { label: 'Киберпанк', value: '24' }, + { label: 'Киберспорт', value: '745' }, + { label: 'Кибертрон', value: '2775' }, + { label: 'Киборги', value: '154' }, + { label: 'Киевская Русь', value: '515' }, + { label: 'Кинк на беременность', value: '2281' }, + { label: 'Кинк на волосы', value: '2089' }, + { label: 'Кинк на волосы на теле', value: '1337' }, + { label: 'Кинк на девственность', value: '2282' }, + { label: 'Кинк на инвалидность', value: '633' }, + { label: 'Кинк на интеллект', value: '2798' }, + { label: 'Кинк на клыки', value: '2325' }, + { label: 'Кинк на колготки', value: '2644' }, + { label: 'Кинк на крылья', value: '2035' }, + { label: 'Кинк на латекс', value: '134' }, + { label: 'Кинк на мольбы', value: '1543' }, + { label: 'Кинк на наручники', value: '1065' }, + { label: 'Кинк на нижнее белье', value: '145' }, + { label: 'Кинк на очки', value: '1723' }, + { label: 'Кинк на ошейники', value: '388' }, + { label: 'Кинк на полноту', value: '552' }, + { label: 'Кинк на похвалу', value: '1042' }, + { label: 'Кинк на протезы', value: '2682' }, + { label: 'Кинк на рога', value: '2320' }, + { label: 'Кинк на руки', value: '915' }, + { label: 'Кинк на сердцебиение', value: '941' }, + { label: 'Кинк на силу', value: '1476' }, + { label: 'Кинк на слезы', value: '1373' }, + { label: 'Кинк на служение', value: '2980' }, + { label: 'Кинк на страх', value: '2246' }, + { label: 'Кинк на стыд', value: '747' }, + { label: 'Кинк на унижение', value: '2291' }, + { label: 'Кинк на униформу', value: '1350' }, + { label: 'Кинк на уши', value: '2370' }, + { label: 'Кинк на формальную одежду', value: '1064' }, + { label: 'Кинк на чужую одежду', value: '2920' }, + { label: 'Кинк на чулки', value: '810' }, + { label: 'Кинк на шрамы', value: '2831' }, + { label: 'Кинк на щекотку', value: '2396' }, + { label: 'Кинк-бинго', value: '2290' }, + { label: 'Кинки / Фетиши', value: '1659' }, + { label: 'Кинотеатры', value: '981' }, + { label: 'Киригакуре', value: '2475' }, + { label: 'Китай', value: '757' }, + { label: 'Кладбища', value: '501' }, + { label: 'Кланы', value: '2879' }, + { label: 'Классизм', value: '1927' }, + { label: 'Клейма', value: '1548' }, + { label: 'Клептомания', value: '1160' }, + { label: 'Клетка Люцифера', value: '1884' }, + { label: 'Клетки', value: '1069' }, + { label: 'Клиническая смерть', value: '2068' }, + { label: 'Клокпанк / Ветропанк', value: '1812' }, + { label: 'Клоны', value: '358' }, + { label: 'Клоуны', value: '1520' }, + { label: 'Клубы по интересам', value: '1369' }, + { label: 'Клюква', value: '1001' }, + { label: 'Кляпы / Затыкание рта', value: '379' }, + { label: 'Книги', value: '2746' }, + { label: 'Книжные магазины', value: '444' }, + { label: 'Кноттинг', value: '99' }, + { label: 'Ковбои', value: '330' }, + { label: 'Кода', value: '1943' }, + { label: 'Кожаная одежда', value: '1517' }, + { label: 'Колдовстворец', value: '2600' }, + { label: 'Кома', value: '516' }, + { label: 'Командная работа', value: '708' }, + { label: 'Комики', value: '1315' }, + { label: 'Комиксисты / Мангаки', value: '576' }, + { label: 'Коммунальные квартиры', value: '1437' }, + { label: 'Коммуны', value: '797' }, + { label: 'Комнатные растения', value: '1792' }, + { label: 'Комплекс Бога', value: '2087' }, + { label: 'Комплекс превосходства', value: '2191' }, + { label: 'Комплексы', value: '2529' }, + { label: 'Конкурс от BUBBLE', value: '2937' }, + { label: 'Конный спорт', value: '1378' }, + { label: 'Конохагакуре', value: '2477' }, + { label: 'Конспирология', value: '1911' }, + { label: 'Контрабандисты', value: '2363' }, + { label: 'Контроль / Подчинение', value: '932' }, + { label: 'Контроль памяти', value: '2540' }, + { label: 'Контроль сознания', value: '257' }, + { label: 'Конфликт мировоззрений', value: '2678' }, + { label: 'Концентрационные лагеря', value: '547' }, + { label: 'Концерты / Выступления', value: '1236' }, + { label: 'Конюшни', value: '514' }, + { label: 'Копирование сознания', value: '1231' }, + { label: 'Копирование способностей', value: '1825' }, + { label: 'Копро-кинк', value: '763' }, + { label: 'Корё', value: '1142' }, + { label: 'Корейская война', value: '2187' }, + { label: 'Коренные народы', value: '1505' }, + { label: 'Королевства', value: '2794' }, + { label: 'Корпорация BOBSOC', value: '2940' }, + { label: 'Корректирующее изнасилование', value: '2856' }, + { label: 'Коррупция', value: '2150' }, + { label: 'Косвенные убийства', value: '2846' }, + { label: 'Космоопера', value: '48' }, + { label: 'Космос', value: '236' }, + { label: 'Косплееры', value: '944' }, + { label: 'Кочевники', value: '1793' }, + { label: 'Кошмары', value: '1228' }, + { label: 'Красная нить судьбы', value: '608' }, + { label: 'Крепости / Башни', value: '2321' }, + { label: 'Крестовые походы', value: '2070' }, + { label: 'Крестражи', value: '2238' }, + { label: 'Кризис гендера', value: '1406' }, + { label: 'Кризис ориентации', value: '1405' }, + { label: 'Криминалистическая экспертиза', value: '2361' }, + { label: 'Криминальная пара', value: '1920' }, + { label: 'Криокинез', value: '1804' }, + { label: 'Криптиды', value: '60' }, + { label: 'Криптоистория', value: '179' }, + { label: 'Кровь / Травмы', value: '800' }, + { label: 'Кроссдрессинг', value: '65' }, + { label: 'Кроссовер', value: '10' }, + { label: 'Кроули — Архангел Рафаэль', value: '2471' }, + { label: 'Круизы', value: '2488' }, + { label: 'Крушение «Титаника»', value: '2200' }, + { label: 'Крылатые', value: '92' }, + { label: 'Крэк', value: '1408' }, + { label: 'Ксенофилия', value: '1711' }, + { label: 'Ксенофобия', value: '589' }, + { label: 'Куба', value: '773' }, + { label: 'Кудэрэ', value: '1216' }, + { label: 'Куклы', value: '1221' }, + { label: 'Куколдинг', value: '524' }, + { label: 'Культ Дракона', value: '2244' }, + { label: 'Кумогакуре', value: '2474' }, + { label: 'Куннилингус', value: '172' }, + { label: 'Купальни', value: '964' }, + { label: 'Курение', value: '202' }, + { label: 'Курортный роман', value: '181' }, + { label: 'Курьеры', value: '2735' }, + { label: 'Лабиринты', value: '1981' }, + { label: 'Лаборатории', value: '1115' }, + { label: 'Лабораторные опыты', value: '146' }, + { label: 'Лав-отели', value: '801' }, + { label: 'Лавандовый брак', value: '1357' }, + { label: 'Лакросс', value: '830' }, + { label: 'Лапслок', value: '267' }, + { label: 'Латентная гомосексуальность', value: '2339' }, + { label: 'ЛГБТ-родители', value: '2723' }, + { label: 'Лебединая верность', value: '537' }, + { label: 'Левитация', value: '1281' }, + { label: 'Легкая атлетика', value: '1797' }, + { label: 'Лекарственная зависимость', value: '2632' }, + { label: 'Леса', value: '703' }, + { label: 'Летние ОтМетки', value: '2956' }, + { label: 'Летние школы', value: '2803' }, + { label: 'Лето девяносто девятого', value: '1990' }, + { label: 'Лжебоги', value: '2869' }, + { label: 'Лингвистический кинк', value: '2042' }, + { label: 'Лирика', value: '2005' }, + { label: 'Лихтенштейн', value: '1023' }, + { label: 'Личность против системы', value: '1912' }, + { label: 'Ложная беременность', value: '1428' }, + { label: 'Ложные воспоминания', value: '376' }, + { label: 'Ложные обвинения', value: '1204' }, + { label: 'Локальный постапокалипсис', value: '2455' }, + { label: 'Лолита (субкультура)', value: '1207' }, + { label: 'Ломка', value: '1846' }, + { label: 'Лошади', value: '1083' }, + { label: 'Лугнасад', value: '2621' }, + { label: 'Лудомания', value: '947' }, + { label: 'Лучники', value: '1521' }, + { label: 'Любить луну', value: '2901' }, + { label: 'Любовная магия', value: '2317' }, + { label: 'Любовный магнит', value: '1407' }, + { label: 'Любовный многоугольник', value: '63' }, + { label: 'Любовь с первого взгляда', value: '813' }, + { label: 'Любовь/Ненависть', value: '1703' }, + { label: 'Люди', value: '1538' }, + { label: 'Людоеды', value: '1288' }, + { label: 'Магазины', value: '1063' }, + { label: 'Магическая связь', value: '2720' }, + { label: 'Магические клятвы', value: '2245' }, + { label: 'Магические лавки', value: '2629' }, + { label: 'Магические учебные заведения', value: '1338' }, + { label: 'Магический реализм', value: '9' }, + { label: 'Магия', value: '1419' }, + { label: 'Магия крови', value: '2857' }, + { label: 'Маленькие города', value: '221' }, + { label: 'Малфоигуд', value: '1890' }, + { label: 'Мамми-кинк', value: '273' }, + { label: 'Мандалорцы', value: '2870' }, + { label: 'Манекены', value: '1509' }, + { label: 'Манипуляции', value: '992' }, + { label: 'Марафонный секс', value: '316' }, + { label: 'Маргиналы', value: '394' }, + { label: 'Марди Гра', value: '2407' }, + { label: 'Маскарады / Балы', value: '443' }, + { label: 'Массаж', value: '2218' }, + { label: 'Мастурбация', value: '226' }, + { label: 'Матриархат', value: '706' }, + { label: 'Матчасть', value: '2328' }, + { label: 'Махотокоро', value: '2943' }, + { label: 'Маяки', value: '1356' }, + { label: 'Мегаломания', value: '985' }, + { label: 'Мегаполисы', value: '451' }, + { label: 'Медицинский кинк', value: '748' }, + { label: 'Медицинское использование наркотиков', value: '925' }, + { label: 'Медовый месяц', value: '2235' }, + { label: 'Медсестры / Медбратья', value: '2236' }, + { label: 'Межбедренный секс', value: '931' }, + { label: 'Междумирье', value: '2280' }, + { label: 'Межэтнические отношения', value: '2121' }, + { label: 'Мейлдом', value: '59' }, + { label: 'Мексика', value: '768' }, + { label: 'Меланхолия', value: '2108' }, + { label: 'Мелодрама', value: '2881' }, + { label: 'Менструация', value: '1570' }, + { label: 'Ментальная тюрьма', value: '2294' }, + { label: 'Месть', value: '907' }, + { label: 'Металлисты', value: '1206' }, + { label: 'Металлокинез', value: '1532' }, + { label: 'Метафизические существа', value: '2097' }, + { label: 'Метки', value: '1398' }, + { label: 'Метросексуалы', value: '558' }, + { label: 'Механофилия / Технофилия', value: '587' }, + { label: 'Мечники', value: '2783' }, + { label: 'Мечты', value: '993' }, + { label: 'Миддлпанк', value: '2076' }, + { label: 'Мизофилия', value: '1970' }, + { label: 'Микро / Макро', value: '407' }, + { label: 'Микрофикшен', value: '417' }, + { label: 'Мимикрия', value: '2083' }, + { label: 'Минет', value: '231' }, + { label: 'Министерство Магии', value: '1917' }, + { label: 'Минотавры', value: '2820' }, + { label: 'Мир без гомофобии', value: '2660' }, + { label: 'Мир Желаний (Однажды в сказке)', value: '2923' }, + { label: 'Мир Института Войны', value: '2379' }, + { label: 'Мироустройство', value: '989' }, + { label: 'Мистерия', value: '2012' }, + { label: 'Мистика', value: '1671' }, + { label: 'Мистическая беременность', value: '1784' }, + { label: 'Мистические защитники', value: '2162' }, + { label: 'Мифы и мифология', value: '18' }, + { label: 'Мнимая беременность', value: '1096' }, + { label: 'Мнимая проза', value: '1098' }, + { label: 'Мнимая свобода', value: '2708' }, + { label: 'Многодетные семьи', value: '2785' }, + { label: 'Множественное проникновение', value: '2059' }, + { label: 'Множественные оргазмы', value: '317' }, + { label: 'Множественные финалы', value: '312' }, + { label: 'Могучие Рейнджеры: Дино Заряд', value: '2931' }, + { label: 'Могучие Рейнджеры: Мегафорс', value: '2930' }, + { label: 'Могучие Рейнджеры: Мистическая Сила', value: '2933' }, + { label: 'Могучие Рейнджеры: Самураи', value: '2929' }, + { label: 'Могучие Рейнджеры: Успеть на помощь', value: '2934' }, + { label: 'Могучие Рейнджеры: Ярость Джунглей', value: '2932' }, + { label: 'Модели', value: '399' }, + { label: 'Модельеры', value: '598' }, + { label: 'Модельный бизнес', value: '435' }, + { label: 'Модификации тела', value: '890' }, + { label: 'Молдова', value: '2967' }, + { label: 'Молчание', value: '2465' }, + { label: 'Монастыри', value: '352' }, + { label: 'Монахи', value: '353' }, + { label: 'Монолог', value: '2333' }, + { label: 'Монстрофилия', value: '94' }, + { label: 'Монстры', value: '529' }, + { label: 'Моральные дилеммы', value: '959' }, + { label: 'Морлед', value: '2255' }, + { label: 'Моря / Океаны', value: '955' }, + { label: 'Моряки', value: '406' }, + { label: 'Мотоспорт', value: '1461' }, + { label: 'Мужская беременность', value: '1660' }, + { label: 'Мужская дружба', value: '2157' }, + { label: 'Мужское грудное кормление', value: '297' }, + { label: 'Музеи / Галереи', value: '795' }, + { label: 'Музы', value: '2001' }, + { label: 'Музыканты', value: '141' }, + { label: 'Муковисцидоз', value: '2764' }, + { label: 'Мультикроссовер', value: '2247' }, + { label: 'Мумии', value: '579' }, + { label: 'Мутанты', value: '740' }, + { label: 'Мэри Сью (Марти Стью)', value: '1655' }, + { label: 'Мятежи / Восстания', value: '584' }, + { label: 'На грани жизни и смерти', value: '2545' }, + { label: 'Наблюдатели', value: '1816' }, + { label: 'Навязчивая опека', value: '954' }, + { label: 'Навязчивые мысли', value: '2851' }, + { label: 'Наги', value: '90' }, + { label: 'Надежда', value: '2627' }, + { label: 'Наемники', value: '512' }, + { label: 'Наемные убийцы', value: '192' }, + { label: 'Названные сиблинги', value: '2927' }, + { label: 'Намеки на отношения', value: '1387' }, + { label: 'Намеки на секс', value: '1382' }, + { label: 'Напарники', value: '1291' }, + { label: 'Наперегонки со временем', value: '2240' }, + { label: 'НапиСанта', value: '2946' }, + { label: 'Наполеоновские войны', value: '470' }, + { label: 'Нарколепсия', value: '586' }, + { label: 'Наркоторговля', value: '1116' }, + { label: 'Нарушение этических норм', value: '968' }, + { label: 'Нарциссизм', value: '546' }, + { label: 'Насекомые', value: '2358' }, + { label: 'Насилие', value: '1649' }, + { label: 'Насилие над детьми', value: '726' }, + { label: 'Наследие (Гарри Поттер)', value: '1935' }, + { label: 'Наследство', value: '2172' }, + { label: 'Наставничество', value: '935' }, + { label: 'Настольные игры', value: '1513' }, + { label: 'Настольные ролевые игры', value: '644' }, + { label: 'Натуризм / Нудизм', value: '1810' }, + { label: 'Наука', value: '2624' }, + { label: 'Научная фантастика', value: '596' }, + { label: 'Научно-исследовательские организации', value: '1503' }, + { label: 'Научное фэнтези', value: '180' }, + { label: 'Начало отношений', value: '1328' }, + { label: 'Небинарные персонажи', value: '314' }, + { label: 'Невезение', value: '2689' }, + { label: 'Невзаимные предначертанные', value: '1484' }, + { label: 'Невзаимные чувства', value: '105' }, + { label: 'Невидимость', value: '359' }, + { label: 'Невидимый мир', value: '2286' }, + { label: 'Неграфичные описания', value: '2855' }, + { label: 'Недоверие', value: '1842' }, + { label: 'Недопонимания', value: '614' }, + { label: 'Недостатки внешности', value: '923' }, + { label: 'Нежелательная беременность', value: '715' }, + { label: 'Нежелательные сверхспособности', value: '1335' }, + { label: 'Нежелательные чувства', value: '2078' }, + { label: 'Нежный секс', value: '280' }, + { label: 'Незащищенный секс', value: '497' }, + { label: 'Нездоровые механизмы преодоления', value: '1528' }, + { label: 'Нездоровые отношения', value: '71' }, + { label: 'Нездоровый образ жизни', value: '2148' }, + { label: 'Нездоровый BDSM', value: '1062' }, + { label: 'Неидеальный омегаверс', value: '2712' }, + { label: 'Неизвестность', value: '2826' }, + { label: 'Неизвестные родственники', value: '2752' }, + { label: 'Неизлечимые заболевания', value: '1269' }, + { label: 'Нейтрализация сверхспособностей', value: '2343' }, + { label: 'Некромаги', value: '679' }, + { label: 'Некрофилия', value: '1663' }, + { label: 'Нелинейное повествование', value: '1699' }, + { label: 'Неловкий секс', value: '2883' }, + { label: 'Неловкость', value: '2459' }, + { label: 'Нелюбящие родители', value: '2817' }, + { label: 'Немертвые', value: '495' }, + { label: 'Немота', value: '414' }, + { label: 'Ненависть', value: '685' }, + { label: 'Ненависть к себе', value: '1441' }, + { label: 'Ненависть с первого взгляда', value: '814' }, + { label: 'Ненадежный рассказчик', value: '322' }, + { label: 'Ненасильственная смерть', value: '2531' }, + { label: 'Необитаемые острова', value: '1196' }, + { label: 'Необратимые превращения', value: '926' }, + { label: 'Неозвученные чувства', value: '258' }, + { label: 'Неприятие отношений', value: '2056' }, + { label: 'Неравные отношения', value: '545' }, + { label: 'Неравный брак', value: '409' }, + { label: 'Неразрывная связь', value: '2569' }, + { label: 'Нервный срыв', value: '1190' }, + { label: 'Нерды', value: '481' }, + { label: 'Нереалистичность', value: '2350' }, + { label: 'Несексуальная близость', value: '2874' }, + { label: 'Нестандартная поэзия', value: '1704' }, + { label: 'Несчастливые отношения', value: '2080' }, + { label: 'Несчастливый финал', value: '61' }, + { label: 'Несчастные случаи', value: '499' }, + { label: 'Нетаймори', value: '2323' }, + { label: 'Неторопливое повествование', value: '1815' }, + { label: 'Нетрадиционное использование Силы', value: '2520' }, + { + label: 'Неумышленное употребление наркотических веществ', + value: '1192', + }, + { label: 'Неуставные отношения', value: '1396' }, + { label: 'Нефилимы', value: '1086' }, + { label: 'Неформальный брак', value: '1355' }, + { label: 'Нецензурная лексика', value: '1653' }, + { label: 'Нечеловеческая мораль', value: '2551' }, + { label: 'Нечеловеческие виды', value: '995' }, + { label: 'Нечистая сила', value: '1464' }, + { label: 'Нидерланды', value: '1141' }, + { label: 'Низкая самооценка', value: '2014' }, + { label: 'Низкое фэнтези', value: '2007' }, + { label: 'Нимфы', value: '1477' }, + { label: 'Ниндзя', value: '471' }, + { label: 'Новая жизнь', value: '2018' }, + { label: 'Новая Зеландия', value: '1789' }, + { label: 'Новеллизация', value: '262' }, + { label: 'Новые отношения', value: '1827' }, + { label: 'Новый год', value: '206' }, + { label: 'Новый канон ЗВ', value: '1948' }, + { label: 'Норны', value: '2206' }, + { label: 'Ностальгия', value: '878' }, + { label: 'Ночное видение', value: '2006' }, + { label: 'Ночные клубы', value: '531' }, + { label: 'Нуар', value: '137' }, + { label: 'Няни', value: '570' }, + { label: 'Обездвиживание', value: '116' }, + { label: 'Обещания / Клятвы', value: '1823' }, + { label: 'Обиды', value: '905' }, + { label: 'Обман / Заблуждение', value: '650' }, + { label: 'Обмен квами', value: '2572' }, + { label: 'Обмен ролями', value: '1055' }, + { label: 'Обмен телами', value: '49' }, + { label: 'Обмороки', value: '2484' }, + { label: 'Обнажение', value: '970' }, + { label: 'Оборотни', value: '28' }, + { label: 'Обоснованный ООС', value: '2549' }, + { label: 'Образ тела', value: '2289' }, + { label: 'Обратный омегаверс', value: '2435' }, + { label: 'Обретенные семьи', value: '601' }, + { label: 'Обреченные отношения', value: '653' }, + { label: 'Обскуры', value: '2237' }, + { label: 'Обсуждение кинков', value: '1072' }, + { label: 'Обусловленная контекстом гомофобия', value: '2685' }, + { label: 'Обусловленный контекстом расизм', value: '2686' }, + { label: 'Обусловленный контекстом сексизм', value: '2687' }, + { label: 'Общежития', value: '534' }, + { label: 'Общественный транспорт', value: '615' }, + { label: 'Объективация', value: '1837' }, + { label: 'Объектофилия', value: '366' }, + { label: 'Овипозиция', value: '269' }, + { label: 'Огнестрельное оружие', value: '1189' }, + { label: 'Ограбления', value: '2269' }, + { label: 'Одержимость', value: '2248' }, + { label: 'Одержимые', value: '496' }, + { label: 'Один день', value: '2647' }, + { label: 'Одиночество', value: '1015' }, + { label: 'Одичавшие дети', value: '835' }, + { label: 'Однолюбы', value: '2146' }, + { label: 'Одноминутный канонический персонаж', value: '2184' }, + { label: 'Однополый мир', value: '1462' }, + { label: 'Ожидание', value: '2909' }, + { label: 'ОЖП', value: '1657' }, + { label: 'Окаменение', value: '2052' }, + { label: 'Океания', value: '1058' }, + { label: 'Оккультизм', value: '2374' }, + { label: 'ОКР', value: '594' }, + { label: 'Окулолинктус', value: '1470' }, + { label: 'Олимпийские игры', value: '1127' }, + { label: 'Олфактофилия', value: '1945' }, + { label: 'Омегаверс', value: '1693' }, + { label: 'Омегаверс: Альфа/Альфа', value: '2437' }, + { label: 'Омегаверс: Альфа/Бета', value: '2741' }, + { label: 'Омегаверс: Альфа/Бета/Омега', value: '2898' }, + { label: 'Омегаверс: Альфа/Омега/Альфа', value: '2436' }, + { label: 'Омегаверс: Бета/Бета', value: '2966' }, + { label: 'Омегаверс: Больше трех полов', value: '2782' }, + { label: 'Омегаверс: Омега/Альфа/Омега', value: '2544' }, + { label: 'Омегаверс: Омега/Бета', value: '2965' }, + { label: 'Омегаверс: Омега/Омега', value: '2564' }, + { label: 'Омегаверс: Смена второго пола', value: '2776' }, + { label: 'Омегаверс: f!Альфа/m!Омега', value: '2777' }, + { label: 'ОМП', value: '1656' }, + { label: 'Онейрокинез', value: '2067' }, + { label: 'Онкологические заболевания', value: '540' }, + { label: 'ООС', value: '1648' }, + { label: 'Опасность', value: '2847' }, + { label: 'Опера', value: '549' }, + { label: 'Опричнина', value: '2252' }, + { label: 'Оптимизм', value: '2420' }, + { label: 'Оракулы / Провидцы', value: '2221' }, + { label: 'Оральная фиксация', value: '2523' }, + { label: 'Оргазм без стимуляции', value: '1000' }, + { label: 'Организаторы свадеб', value: '858' }, + { label: 'Орден Феникса', value: '1907' }, + { label: 'Орки', value: '187' }, + { label: 'Оружие массового поражения', value: '1193' }, + { label: 'Освоение земель', value: '1824' }, + { label: 'Оседлание', value: '1223' }, + { label: 'Османская империя', value: '527' }, + { label: 'Особняки / Резиденции', value: '500' }, + { label: 'Осознанные сновидения', value: '530' }, + { label: 'Остеокинез', value: '2845' }, + { label: 'Острова', value: '799' }, + { label: 'От антигероя к герою', value: '2173' }, + { label: 'От антигероя к злодею', value: '2213' }, + { label: 'От божественного существа к смертному', value: '2664' }, + { label: 'От возлюбленных к врагам', value: '783' }, + { label: 'От возлюбленных к друзьям', value: '782' }, + { label: 'От врагов к возлюбленным', value: '346' }, + { label: 'От врагов к друзьям', value: '347' }, + { label: 'От врагов к друзьям к возлюбленным', value: '345' }, + { label: 'От героя к антигерою', value: '2546' }, + { label: 'От героя к злодею', value: '1027' }, + { label: 'От героя к монстру', value: '2659' }, + { label: 'От героя к обычному персонажу', value: '2584' }, + { label: 'От друзей к возлюбленным', value: '344' }, + { label: 'От друзей к врагам', value: '463' }, + { label: 'От друзей к незнакомцам', value: '1870' }, + { label: 'От злодея к антигерою', value: '2640' }, + { label: 'От злодея к герою', value: '1026' }, + { label: 'От напарников к друзьям к возлюбленным', value: '2460' }, + { label: 'От нездоровых отношений к здоровым', value: '2301' }, + { label: 'От незнакомцев к возлюбленным', value: '372' }, + { label: 'От простонародья к аристократу', value: '2816' }, + { label: 'От сексуальных партнеров к возлюбленным', value: '2069' }, + { label: 'От смертного к божественному существу', value: '2665' }, + { label: 'От супругов к возлюбленным', value: '2359' }, + { label: 'Ответвление от канона', value: '2336' }, + { label: 'Ответственность', value: '2468' }, + { label: 'Отдаю на вдохновение', value: '2288' }, + { label: 'Отечественная война 1812 года', value: '1162' }, + { label: 'Отказ от чувств', value: '2227' }, + { label: 'Отклонения от канона', value: '5' }, + { label: 'Открытый финал', value: '185' }, + { label: 'ОтМетки', value: '2945' }, + { label: 'Отношения втайне', value: '285' }, + { label: 'Отношения на расстоянии', value: '37' }, + { label: 'Отношения на спор', value: '383' }, + { label: 'Отношения наполовину', value: '1573' }, + { label: 'Отпуск', value: '1828' }, + { label: 'Отрицание', value: '1868' }, + { label: 'Отрицание чувств', value: '648' }, + { label: 'Отрицательный протагонист', value: '1442' }, + { label: 'Отсутствие души', value: '1897' }, + { label: 'Отшельники', value: '543' }, + { label: 'Офисы', value: '74' }, + { label: 'Официанты / Официантки', value: '2560' }, + { label: 'Охота', value: '825' }, + { label: 'Охота на разумных существ', value: '2784' }, + { label: 'Охотники', value: '2036' }, + { label: 'Охотники за головами', value: '2103' }, + { label: 'Охотники на нечисть', value: '46' }, + { label: 'Охранники', value: '1173' }, + { label: 'Ошибки', value: '2554' }, + { label: 'Ошибочная идентификация', value: '2490' }, + { label: 'Паладины', value: '1194' }, + { label: 'Пан-персонажи', value: '302' }, + { label: 'Пандемия COVID-19', value: '2724' }, + { label: 'Панические атаки', value: '751' }, + { label: 'Паническое расстройство', value: '733' }, + { label: 'Панки', value: '625' }, + { label: 'Паразиты', value: '1877' }, + { label: 'Паралич', value: '1941' }, + { label: 'Параллельные миры', value: '378' }, + { label: 'Паранойя', value: '1253' }, + { label: 'Пари', value: '778' }, + { label: 'Парки / Зоны отдыха', value: '1005' }, + { label: 'Парки аттракционов / Ярмарки', value: '793' }, + { label: 'Паркур', value: '553' }, + { label: 'Пародия', value: '1676' }, + { label: 'Партизаны', value: '850' }, + { label: 'Парфюмеры', value: '1384' }, + { label: 'Пасха', value: '562' }, + { label: 'Патологическое бесстрашие', value: '1498' }, + { label: 'Патологическое накопительство', value: '2691' }, + { label: 'Патологоанатомы', value: '502' }, + { label: 'Патриархат', value: '705' }, + { label: 'Патриотические темы и мотивы', value: '2231' }, + { label: 'Патронусы', value: '2390' }, + { label: 'Патрули', value: '1819' }, + { label: 'Пафос', value: '2589' }, + { label: 'Пацифисты', value: '1939' }, + { label: 'Пегасы', value: '1047' }, + { label: 'Пеггинг', value: '159' }, + { label: 'Пейзажная лирика', value: '2330' }, + { label: 'Пекарни', value: '78' }, + { label: 'Первая мировая', value: '487' }, + { label: 'Первая чеченская война', value: '2428' }, + { label: 'Первобытные времена', value: '465' }, + { label: 'Первое апреля', value: '2315' }, + { label: 'Первопроходцы', value: '351' }, + { label: 'Первый контакт', value: '2093' }, + { label: 'Первый поцелуй', value: '2790' }, + { label: 'Первый раз', value: '1292' }, + { label: 'Передача магических способностей', value: '2497' }, + { label: 'Переезд', value: '658' }, + { label: 'Перезапуск мира', value: '2710' }, + { label: 'Перемирие', value: '1331' }, + { label: 'Переписки и чаты (стилизация)', value: '38' }, + { label: 'Перерыв в отношениях', value: '519' }, + { label: 'Переселение душ', value: '1237' }, + { label: 'Перестрелки', value: '2268' }, + { label: 'Переходный возраст', value: '1297' }, + { label: 'Период Эдо', value: '1140' }, + { label: 'Персекуторный бред', value: '2136' }, + { label: 'Персонажи-геи', value: '2141' }, + { label: 'Персонажи-лесбиянки', value: '2142' }, + { label: 'Персонификация', value: '994' }, + { label: 'Персонификация смерти', value: '1046' }, + { label: 'Перу', value: '2017' }, + { label: 'Перфекционизм', value: '2730' }, + { label: 'Пессимизм', value: '2709' }, + { label: 'Пет-плей', value: '391' }, + { label: 'Петербург: бикейм #Чимин Сергеевич', value: '1859' }, + { label: 'Петтинг', value: '301' }, + { label: 'Пещеры', value: '655' }, + { label: 'Пижамные вечеринки', value: '1340' }, + { label: 'Пикацизм', value: '1227' }, + { label: 'Пикники', value: '1165' }, + { label: 'Пионеры', value: '668' }, + { label: 'Пираты', value: '72' }, + { label: 'Пирокинез', value: '1180' }, + { label: 'Пиромания', value: '899' }, + { label: 'Пирсинг', value: '507' }, + { label: 'Писатели', value: '196' }, + { label: 'Письма', value: '2298' }, + { label: 'Письма (стилизация)', value: '266' }, + { label: 'Пиццерии / Дайнеры', value: '593' }, + { label: 'Плавание', value: '1135' }, + { label: 'Плантокинез', value: '2029' }, + { label: 'Пластические операции', value: '1847' }, + { label: 'Платонические отношения', value: '385' }, + { label: 'Племена', value: '1336' }, + { label: 'Плен', value: '290' }, + { label: 'Плененные путешествием', value: '2615' }, + { label: 'Плохая компания', value: '1502' }, + { label: 'Плохие друзья', value: '2445' }, + { label: 'Плохой хороший финал', value: '1265' }, + { label: 'Плюшефилия', value: '520' }, + { label: 'Пляжи', value: '766' }, + { label: 'По разные стороны', value: '897' }, + { label: 'Побег', value: '661' }, + { label: 'Побег из дома', value: '2309' }, + { label: 'Победа Волдеморта', value: '1887' }, + { label: 'Победа Гриндевальда', value: '2398' }, + { label: 'Повара', value: '2853' }, + { label: 'Повелитель смерти', value: '1909' }, + { label: 'Повествование в будущем времени', value: '2633' }, + { label: 'Повествование в настоящем времени', value: '1826' }, + { label: 'Повествование во втором лице', value: '166' }, + { label: 'Повествование от нескольких лиц', value: '1157' }, + { label: 'Повествование от первого лица', value: '1681' }, + { label: 'Повседневность', value: '1677' }, + { label: 'Повстанцы', value: '659' }, + { label: 'Повторение судьбы', value: '1997' }, + { label: 'Поглощение способностей', value: '2605' }, + { label: 'Погони / Преследования', value: '1996' }, + { label: 'Пограничный синдром', value: '710' }, + { label: 'Погребение заживо', value: '1279' }, + { label: 'Под одной крышей', value: '462' }, + { label: 'Под старину (стилизация)', value: '2486' }, + { label: 'Подарки', value: '1109' }, + { label: 'Подвешивание', value: '914' }, + { label: 'Подводный мир', value: '901' }, + { label: 'Подземелья', value: '2314' }, + { label: 'Подземный мир', value: '1453' }, + { label: 'Подлость', value: '2625' }, + { label: 'Подменыши', value: '2250' }, + { label: 'Подпольные бои', value: '1389' }, + { label: 'Подразумеваемая смерть персонажа', value: '2204' }, + { label: 'Подростки', value: '218' }, + { label: 'Подростковая беременность', value: '986' }, + { label: 'Подростковая влюбленность', value: '581' }, + { label: 'Подсознание', value: '2273' }, + { label: 'Поедание разумных существ', value: '1795' }, + { label: 'Пожарные', value: '2557' }, + { label: 'Пожилые персонажи', value: '852' }, + { label: 'Поза 69', value: '699' }, + { label: 'Поиск работы', value: '2646' }, + { label: 'Поиск родителей', value: '2033' }, + { label: 'Поклонение телу', value: '712' }, + { label: 'Покровительство', value: '2223' }, + { label: 'Покушение на жизнь', value: '1057' }, + { label: 'Покушение на свободу', value: '2655' }, + { label: 'Полиамория', value: '1705' }, + { label: 'Полиандрия', value: '761' }, + { label: 'Полигиния', value: '760' }, + { label: 'Политика', value: '1835' }, + { label: 'Политики', value: '711' }, + { label: 'Политические интриги', value: '183' }, + { label: 'Полицейские', value: '14' }, + { label: 'Половой диморфизм', value: '1780' }, + { label: 'Полубоги', value: '437' }, + { label: 'Полудемоны', value: '2872' }, + { label: 'Полудраконы', value: '2859' }, + { label: 'Полукровки', value: '368' }, + { label: 'Польша', value: '777' }, + { label: 'Полярные станции', value: '1493' }, + { label: 'Помощь врагу', value: '1325' }, + { label: 'Попаданцы: В реальный мир', value: '1499' }, + { label: 'Попаданцы: В своем теле', value: '1412' }, + { label: 'Попаданцы: В чужом теле', value: '1413' }, + { label: 'Попаданцы: Из одного фандома в другой', value: '2699' }, + { label: 'Попаданцы: Из фандома в ориджинал', value: '2193' }, + { label: 'Попаданцы: По обмену', value: '2196' }, + { label: 'Попаданчество', value: '1695' }, + { label: 'Популярность', value: '2581' }, + { label: 'Попытка изнасилования', value: '1440' }, + { label: 'Порноактеры', value: '289' }, + { label: 'Португалия', value: '1749' }, + { label: 'Порты', value: '1028' }, + { label: 'Поселки', value: '1298' }, + { label: 'После таймскипа (Haikyuu!!)', value: '2906' }, + { label: 'После таймскипа (Shingeki no Kyojin)', value: '2903' }, + { label: 'Послевоенное время', value: '1080' }, + { label: 'Последний раз', value: '2645' }, + { label: 'Последний рубеж', value: '1178' }, + { label: 'Последний шанс', value: '1209' }, + { label: 'Последствия', value: '2491' }, + { label: 'Последствия болезни', value: '2612' }, + { label: 'Послеродовая депрессия', value: '988' }, + { label: 'Посмертная любовь', value: '2670' }, + { label: 'Посмертный персонаж', value: '2653' }, + { label: 'Пост-Пацифист', value: '2408' }, + { label: 'Пост-Рейхенбах', value: '1903' }, + { label: 'Пост-третий сезон (Шерлок)', value: '1977' }, + { label: 'Постапокалиптика', value: '1697' }, + { label: 'ПостВБ', value: '2386' }, + { label: 'Постканон', value: '6' }, + { label: 'ПостРагнарек', value: '2537' }, + { label: 'ПостХог', value: '1883' }, + { label: 'ПостЭндгейм', value: '2179' }, + { label: 'Потеря девственности', value: '1234' }, + { label: 'Потеря конечностей', value: '1289' }, + { label: 'Потеря магических способностей', value: '1430' }, + { label: 'Потеря памяти', value: '36' }, + { label: 'Потеря сверхспособностей', value: '1051' }, + { label: 'Поттергад', value: '2417' }, + { label: 'Похищение', value: '224' }, + { label: 'Похороны', value: '786' }, + { label: 'Почтовые работники', value: '1821' }, + { label: 'Пошлый юмор', value: '1362' }, + { label: 'Поэма', value: '1938' }, + { label: 'Поэты', value: '357' }, + { label: 'Пояс верности', value: '1779' }, + { label: 'Правда или действие', value: '1982' }, + { label: 'Практопия', value: '2249' }, + { label: 'Пре-гет', value: '209' }, + { label: 'Пре-Рош', value: '2047' }, + { label: 'Пре-слэш', value: '207' }, + { label: 'Пре-фемслэш', value: '208' }, + { label: 'Предательство', value: '361' }, + { label: 'Предвидение', value: '1876' }, + { label: 'Предложение руки и сердца', value: '1081' }, + { label: 'Предопределенность', value: '411' }, + { label: 'Предрассудки', value: '2046' }, + { label: 'Предсмертные сообщения', value: '2174' }, + { label: 'Преканон', value: '7' }, + { label: 'ПреКербер', value: '2726' }, + { label: 'Прекрасная эпоха', value: '1809' }, + { label: 'Прелюдия', value: '2395' }, + { label: 'Пренебрежение гигиеной', value: '2921' }, + { label: 'Пренебрежение жизнью', value: '2216' }, + { label: 'Преодоление комплексов', value: '588' }, + { label: 'Преподаватели', value: '403' }, + { label: 'Преподаватель/Обучающийся', value: '2507' }, + { label: 'Преступники', value: '1446' }, + { label: 'Преступный мир', value: '12' }, + { label: 'Прибрежные города', value: '984' }, + { label: 'Приватный танец', value: '2786' }, + { label: 'Привязанность', value: '2372' }, + { label: 'Пригород', value: '452' }, + { label: 'Приемные семьи', value: '844' }, + { label: 'Признания в любви', value: '1049' }, + { label: 'Призраки', value: '26' }, + { label: 'Приключения', value: '21' }, + { label: 'Приметы / Суеверия', value: '612' }, + { label: 'Примирение', value: '660' }, + { label: 'Принудительная феминизация', value: '656' }, + { label: 'Принудительное лечение', value: '971' }, + { label: 'Принудительные отношения', value: '2302' }, + { label: 'Принудительный брак', value: '307' }, + { label: 'Принудительный инцест', value: '2942' }, + { label: 'Принуждение', value: '1323' }, + { label: 'Принятие себя', value: '2754' }, + { label: 'Прислуга', value: '804' }, + { label: 'Прист-кинк', value: '342' }, + { label: 'Приступы агрессии', value: '2788' }, + { label: 'Притча', value: '1088' }, + { label: 'Приюты для животных', value: '2345' }, + { label: 'ПРЛ', value: '1092' }, + { label: 'Проблемы доверия', value: '2208' }, + { label: 'Проблемы с законом', value: '1007' }, + { label: 'Проводники душ', value: '2348' }, + { label: 'Программисты', value: '574' }, + { label: 'Прогрессорство', value: '449' }, + { label: 'Прогулки', value: '2462' }, + { label: 'Продажа души', value: '887' }, + { label: 'Прозопагнозия', value: '2013' }, + { label: 'Производственный роман', value: '2119' }, + { label: 'Проклятия', value: '875' }, + { label: 'Пролапс', value: '2926' }, + { label: 'Промискуитет', value: '1817' }, + { label: 'Промышленные предприятия', value: '1456' }, + { label: 'Пропавшие без вести', value: '2073' }, + { label: 'Пропущенная сцена', value: '1702' }, + { label: 'Пророчества', value: '834' }, + { label: 'Просмотр порно', value: '2341' }, + { label: 'Проституция', value: '178' }, + { label: 'Протезы', value: '1581' }, + { label: 'Противоположности', value: '948' }, + { label: 'Противоречивые чувства', value: '2528' }, + { label: 'Прошлое', value: '983' }, + { label: 'Прощение', value: '1545' }, + { label: 'Псевдо-инцест', value: '336' }, + { label: 'Псевдо-мистика', value: '2159' }, + { label: 'Псевдоисторический сеттинг', value: '1479' }, + { label: 'Псионика', value: '2023' }, + { label: 'Психиатрические больницы', value: '135' }, + { label: 'Психические расстройства', value: '69' }, + { label: 'Психоз', value: '1339' }, + { label: 'Психологи / Психоаналитики', value: '611' }, + { label: 'Психологическая война', value: '2595' }, + { label: 'Психологические пытки', value: '2825' }, + { label: 'Психологические травмы', value: '40' }, + { label: 'Психологический мазохизм', value: '2212' }, + { label: 'Психологический ужас', value: '1261' }, + { label: 'Психологическое насилие', value: '836' }, + { label: 'Психология', value: '1674' }, + { label: 'Психопатия', value: '869' }, + { label: 'Психосоматические расстройства', value: '1834' }, + { label: 'Психотерапия', value: '958' }, + { label: 'ПТСР', value: '425' }, + { label: 'Публичное обнажение', value: '319' }, + { label: 'Пурпурная проза', value: '599' }, + { label: 'Пустоши', value: '1114' }, + { label: 'Пустыни', value: '692' }, + { label: 'Путешественники-исследователи', value: '888' }, + { label: 'Путешествия', value: '894' }, + { label: 'Пытки', value: '210' }, + { label: 'Пьеса', value: '434' }, + { label: 'Рабочие', value: '1771' }, + { label: 'Рабство', value: '51' }, + { label: 'Равнодушие', value: '2516' }, + { label: 'Равные отношения', value: '2728' }, + { label: 'Радикальная медицина', value: '731' }, + { label: 'Радио', value: '974' }, + { label: 'Разбойники', value: '473' }, + { label: 'Развитие отношений', value: '1404' }, + { label: 'Развод', value: '200' }, + { label: 'Разговоры', value: '1401' }, + { label: 'Раздвоение личности', value: '287' }, + { label: 'Раздельное обучение', value: '1959' }, + { label: 'Разлука / Прощания', value: '1104' }, + { label: 'Разница в возрасте', value: '2' }, + { label: 'Разница культур', value: '1079' }, + { label: 'Разновозрастная дружба', value: '2402' }, + { label: 'Разнополая дружба', value: '806' }, + { label: 'Разоблачения', value: '2684' }, + { label: 'Разочарования', value: '646' }, + { label: 'Разрушение стереотипов', value: '2805' }, + { label: 'Разрушение четвертой стены', value: '381' }, + { label: 'Разрыв связи', value: '2582' }, + { label: 'Разум роя', value: '1459' }, + { label: 'Разумное оружие', value: '2675' }, + { label: 'Разумные животные', value: '895' }, + { label: 'Разумные растения', value: '1037' }, + { label: 'Раптофилия', value: '729' }, + { label: 'Расизм', value: '433' }, + { label: 'Раскрытие личностей', value: '2120' }, + { label: 'Раскрытие магии', value: '1878' }, + { label: 'Расплата', value: '2464' }, + { label: 'Рассказ в рассказе', value: '2077' }, + { label: 'Расставание', value: '506' }, + { label: 'Расстройства аутистического спектра', value: '541' }, + { label: 'Расстройства цветового восприятия', value: '1330' }, + { label: 'Расстройства шизофренического спектра', value: '912' }, + { label: 'Растаманы', value: '857' }, + { label: 'Расширенная Вселенная ЗВ', value: '1950' }, + { label: 'Реабилитационные центры', value: '1467' }, + { label: 'Реактивное расстройство привязанности', value: '2576' }, + { label: 'Реакции (стилизация)', value: '1283' }, + { label: 'Реализм', value: '1201' }, + { label: 'Реалити-шоу', value: '2312' }, + { label: 'Ревность', value: '544' }, + { label: 'Революции', value: '585' }, + { label: 'Регенерация', value: '1530' }, + { label: 'Регресс', value: '450' }, + { label: 'Регрессия возраста', value: '102' }, + { label: 'Редкие заболевания', value: '1856' }, + { label: 'Реинкарнация', value: '101' }, + { label: 'Рейтинг за лексику', value: '276' }, + { label: 'Рейтинг за насилие и/или жестокость', value: '277' }, + { label: 'Рейтинг за секс', value: '278' }, + { label: 'Религиозная нетерпимость', value: '2451' }, + { label: 'Религиозные войны', value: '2911' }, + { label: 'Религиозные темы и мотивы', value: '198' }, + { label: 'Религиозный фанатизм', value: '2548' }, + { label: 'Ренессанс', value: '239' }, + { label: 'Репетиторы', value: '2413' }, + { label: 'Репетиции', value: '2241' }, + { label: 'Репродуктивное насилие', value: '2310' }, + { label: 'Реставраторы', value: '2239' }, + { label: 'Рестораны', value: '412' }, + { label: 'Ретеллинг', value: '52' }, + { label: 'Ретрофутуризм', value: '727' }, + { label: 'Реформация', value: '2199' }, + { label: 'Речь Посполитая', value: '1529' }, + { label: 'Риветхеды', value: '2904' }, + { label: 'Римминг', value: '170' }, + { label: 'Риск', value: '2467' }, + { label: 'Рискованная беременность', value: '998' }, + { label: 'Ритуалы', value: '749' }, + { label: 'Ритуальные услуги', value: '2362' }, + { label: 'Роботы', value: '513' }, + { label: 'Родео', value: '1314' }, + { label: 'Родители-одиночки', value: '325' }, + { label: 'Родительские чувства', value: '2308' }, + { label: 'Родомагия', value: '1899' }, + { label: 'Роды', value: '991' }, + { label: 'Рождество', value: '211' }, + { label: 'Розыгрыши', value: '1807' }, + { label: 'Рокеры', value: '861' }, + { label: 'Роковая женщина / Роковой мужчина', value: '982' }, + { label: 'Роковое усилие', value: '2604' }, + { label: 'Роллер-дерби', value: '1014' }, + { label: 'Романтизация', value: '1898' }, + { label: 'Романтика', value: '1664' }, + { label: 'Романтическая дружба', value: '2307' }, + { label: 'Ромком', value: '2768' }, + { label: 'Российская империя', value: '475' }, + { label: 'Россия', value: '701' }, + { label: 'РПП', value: '286' }, + { label: 'Румыния', value: '862' }, + { label: 'Русалки', value: '32' }, + { label: 'Русальная неделя', value: '1067' }, + { label: 'Русреал', value: '53' }, + { label: 'Руферы', value: '578' }, + { label: 'Рыбалка', value: '855' }, + { label: 'Рыцари', value: '426' }, + { label: 'Рыцарские турниры', value: '2759' }, + { label: 'С чистого листа', value: '987' }, + { label: 'Савойя', value: '1036' }, + { label: 'Садизм / Мазохизм', value: '461' }, + { label: 'Садовники', value: '1348' }, + { label: 'Сайз-кинк', value: '100' }, + { label: 'Самобытные культуры', value: '663' }, + { label: 'Самовнушение', value: '2169' }, + { label: 'Самовставка', value: '1689' }, + { label: 'Самозванцы', value: '1811' }, + { label: 'Самоистязание', value: '1386' }, + { label: 'Самоопределение / Самопознание', value: '1278' }, + { label: 'Самопожертвование', value: '750' }, + { label: 'Самосуд', value: '1402' }, + { label: 'Самоуничижение', value: '2667' }, + { label: 'Самураи', value: '472' }, + { label: 'Санатории', value: '223' }, + { label: 'Сандалпанк', value: '1986' }, + { label: 'Сарказм', value: '2225' }, + { label: 'Сатира', value: '1944' }, + { label: 'Сатиры', value: '264' }, + { label: 'Сахарный диабет', value: '2609' }, + { label: 'Сборник драбблов', value: '39' }, + { label: 'Сборник лимериков', value: '1989' }, + { label: 'Сборник пирожков', value: '743' }, + { label: 'Сборник стихотворений', value: '600' }, + { label: 'Сборник японской поэзии', value: '1351' }, + { label: 'Свадьба', value: '184' }, + { label: 'Сверхразумы', value: '966' }, + { label: 'Сверхскорость', value: '1902' }, + { label: 'Сверхспособности', value: '597' }, + { label: 'Светлые властелины', value: '2596' }, + { label: 'Светские мероприятия', value: '973' }, + { label: 'Свидание вслепую', value: '741' }, + { label: 'Свидания', value: '1235' }, + { label: 'Свидетели', value: '1242' }, + { label: 'Свингерство', value: '203' }, + { label: 'Свобода', value: '2166' }, + { label: 'Свободные отношения', value: '255' }, + { label: 'Сводничество', value: '568' }, + { label: 'Сводные родственники', value: '1445' }, + { label: 'Святые', value: '393' }, + { label: 'Сгенерировано ИИ', value: '3014' }, + { label: 'СДВГ', value: '539' }, + { label: 'Севвитус', value: '1871' }, + { label: 'Северная Корея', value: '2767' }, + { label: 'Северус Снейп жив', value: '2406' }, + { label: 'Сегрегация', value: '2158' }, + { label: 'Секретари', value: '1170' }, + { label: 'Секретные миссии', value: '1829' }, + { label: 'Секс без обязательств', value: '387' }, + { label: 'Секс в воде', value: '718' }, + { label: 'Секс в нетрезвом виде', value: '1110' }, + { label: 'Секс в одежде', value: '1013' }, + { label: 'Секс в публичных местах', value: '123' }, + { label: 'Секс в транспорте', value: '124' }, + { label: 'Секс в церкви', value: '2968' }, + { label: 'Секс во время беременности', value: '279' }, + { label: 'Секс во время болезни', value: '1365' }, + { label: 'Секс во время менструации', value: '153' }, + { label: 'Секс на камеру', value: '1101' }, + { label: 'Секс на природе', value: '1256' }, + { label: 'Секс перед зеркалом', value: '1255' }, + { label: 'Секс по расчету', value: '1212' }, + { label: 'Секс по телефону', value: '133' }, + { label: 'Секс при посторонних', value: '2514' }, + { label: 'Секс с воображаемыми партнерами', value: '2276' }, + { label: 'Секс с использованием магии', value: '1563' }, + { + label: 'Секс с использованием одурманивающих веществ', + value: '2635', + }, + { label: 'Секс с использованием посторонних предметов', value: '1661' }, + { label: 'Секс с использованием сверхспособностей', value: '2610' }, + { label: 'Секс со смертельным исходом', value: '1171' }, + { label: 'Секс-игрушки', value: '176' }, + { label: 'Секс-индустрия', value: '447' }, + { label: 'Секс-клубы / Секс-вечеринки', value: '936' }, + { label: 'Секс-куклы', value: '1202' }, + { label: 'Секс-магия', value: '1131' }, + { label: 'Секс-машины', value: '1112' }, + { label: 'Секс-шопы', value: '739' }, + { label: 'Сексизм', value: '464' }, + { label: 'Сексомния', value: '2024' }, + { label: 'Секстинг', value: '2215' }, + { label: 'Сексуальная неопытность', value: '1717' }, + { label: 'Сексуальное насилие', value: '2495' }, + { label: 'Сексуальное обучение', value: '967' }, + { label: 'Сексуальное рабство', value: '1353' }, + { label: 'Сексуальные фобии', value: '1537' }, + { label: 'Селфхарм', value: '76' }, + { label: 'Селфцест', value: '1708' }, + { label: 'Семейная сага', value: '920' }, + { label: 'Семейные тайны', value: '779' }, + { label: 'Семейный бизнес', value: '2338' }, + { label: 'Семьи', value: '220' }, + { label: 'Сенсорная депривация', value: '282' }, + { label: 'Сенсорная перегрузка', value: '2351' }, + { label: 'Сентиментальность', value: '2567' }, + { label: 'Серая мораль', value: '839' }, + { label: 'Серая мышь', value: '1250' }, + { label: 'Серая реальность', value: '2278' }, + { label: 'Серийные насильники', value: '1942' }, + { label: 'Серийные убийцы', value: '193' }, + { label: 'Серфинг', value: '764' }, + { label: 'Сиамские близнецы', value: '1252' }, + { label: 'Сибари', value: '131' }, + { label: 'Сиблинги', value: '617' }, + { label: 'Сидение на лице', value: '713' }, + { label: 'Силкпанк', value: '1813' }, + { label: 'Симбиоз', value: '849' }, + { label: 'Символизм', value: '538' }, + { label: 'Симфорофилия', value: '492' }, + { label: 'Синдром восьмиклассника', value: '2032' }, + { label: 'Синдром выжившего', value: '1100' }, + { label: 'Синдром Дауна', value: '2106' }, + { label: 'Синдром Котара', value: '2765' }, + { label: 'Синдром Питера Пэна', value: '687' }, + { label: 'Синдром Туретта', value: '946' }, + { label: 'Синестезия', value: '960' }, + { label: 'Сирены', value: '1830' }, + { label: 'Сириус Блэк жив', value: '2617' }, + { label: 'Сироты', value: '689' }, + { label: 'Ситком', value: '2553' }, + { label: 'Сказка', value: '288' }, + { label: 'Скайпанк', value: '976' }, + { label: 'Скандинавия', value: '781' }, + { label: 'Скандинавская мифология', value: '2304' }, + { label: 'Сквиртинг', value: '950' }, + { label: 'Сквозное видение', value: '1280' }, + { label: 'Скейтбординг', value: '1395' }, + { label: 'Скелеты', value: '1766' }, + { label: 'Скинхэды', value: '1757' }, + { label: 'Скрытая беременность', value: '2580' }, + { label: 'Скрытность', value: '2577' }, + { label: 'Скрытые способности', value: '2579' }, + { label: 'Скрытый Этаж', value: '2763' }, + { label: 'Скульпторы', value: '2976' }, + { label: 'Слатшейминг', value: '2910' }, + { label: 'Следующее поколение', value: '698' }, + { label: 'Слежка', value: '2102' }, + { label: 'Слепота', value: '413' }, + { label: 'Слияние', value: '2324' }, + { label: 'Словакия', value: '2938' }, + { label: 'Словения', value: '1788' }, + { label: 'Сложные отношения', value: '326' }, + { label: 'Слом личности', value: '623' }, + { label: 'Слоуберн', value: '19' }, + { label: 'Служебные животные', value: '1506' }, + { label: 'Служебные отношения', value: '1347' }, + { label: 'Служебный роман', value: '75' }, + { label: 'Слухи / Сплетни', value: '2809' }, + { label: 'Случайные убийства', value: '2673' }, + { label: 'Случайный брак', value: '304' }, + { label: 'Случайный поцелуй', value: '2852' }, + { label: 'Случайный секс', value: '1275' }, + { label: 'Смена имени', value: '2094' }, + { label: 'Смена мировоззрения', value: '2865' }, + { label: 'Смена сущности', value: '1707' }, + { label: 'Смертельные заболевания', value: '1132' }, + { label: 'Смерть антагониста', value: '2650' }, + { label: 'Смерть всех персонажей', value: '2611' }, + { label: 'Смерть второстепенных персонажей', value: '1713' }, + { label: 'Смерть животных', value: '2210' }, + { label: 'Смерть основных персонажей', value: '1647' }, + { label: 'СМИ', value: '2656' }, + { label: 'Смирение', value: '2272' }, + { label: 'Смотрины', value: '1495' }, + { label: 'Смутное время', value: '2002' }, + { label: 'Снайперы', value: '1762' }, + { label: 'Сновидения', value: '490' }, + { label: 'Сноубординг', value: '2180' }, + { label: 'Соблазнение / Ухаживания', value: '616' }, + { label: 'Собственничество', value: '908' }, + { label: 'Советпанк', value: '397' }, + { label: 'Советский Союз', value: '468' }, + { label: 'Совместная кровать', value: '1229' }, + { label: 'Совместное купание', value: '1301' }, + { label: 'Современное искусство', value: '1087' }, + { label: 'Современность', value: '80' }, + { label: 'Согласование с каноном', value: '1388' }, + { label: 'Сожаления', value: '550' }, + { label: 'Создание общества', value: '2643' }, + { label: 'Созидание', value: '2590' }, + { label: 'Сокровища', value: '893' }, + { label: 'Сокрытие убийства', value: '2311' }, + { label: 'Солянка из Сансов', value: '2515' }, + { label: 'Сомелье / Бармены', value: '865' }, + { label: 'Сомнамбулизм', value: '1394' }, + { label: 'Сомнения', value: '1544' }, + { label: 'Сомнофилия', value: '95' }, + { label: 'Сонный паралич', value: '1203' }, + { label: 'Соперничество', value: '671' }, + { label: 'Сорванная свадьба', value: '2044' }, + { label: 'Сострадание', value: '885' }, + { label: 'Состязания', value: '910' }, + { label: 'Состязания с участием животных', value: '1154' }, + { label: 'Сотриголова — отец Хитоши Шинсо', value: '2877' }, + { label: 'Соулмейты', value: '1696' }, + { label: 'Соулмейты: Действия', value: '2575' }, + { label: 'Соулмейты: Изменение восприятия', value: '1400' }, + { label: 'Соулмейты: Контакт через кожу', value: '2608' }, + { label: 'Соулмейты: Ментальная связь', value: '2841' }, + { label: 'Соулмейты: Общая боль', value: '2583' }, + { label: 'Соулмейты: Общие эмоции', value: '2637' }, + { label: 'Соулмейты: Опознавательные фразы', value: '1399' }, + { label: 'Соулмейты: Предметы', value: '2563' }, + { label: 'Соулмейты: Предпочтения', value: '2521' }, + { label: 'Соулмейты: Разноцветные глаза', value: '2461' }, + { label: 'Соулмейты: Ранения', value: '2134' }, + { label: 'Соулмейты: Сны', value: '2414' }, + { label: 'Соулмейты: Тактильный контакт', value: '2190' }, + { label: 'Соулмейты: Татуировки', value: '2711' }, + { label: 'Соулмейты: Физические проявления', value: '2864' }, + { label: 'Социальная фантастика', value: '591' }, + { label: 'Социальные темы и мотивы', value: '1017' }, + { label: 'Социальные эксперименты', value: '2756' }, + { label: 'Социальный паразитизм', value: '2444' }, + { label: 'Социофобия', value: '826' }, + { label: 'Спарринги', value: '1575' }, + { label: 'Спасатели', value: '392' }, + { label: 'Спасение жизни', value: '1299' }, + { label: 'Спасение мира', value: '812' }, + { label: 'Спектрофилия', value: '2634' }, + { label: 'Спецагенты', value: '401' }, + { label: 'Спиритизм', value: '1478' }, + { label: 'Сплошинг', value: '424' }, + { label: 'Спонтанный секс', value: '2918' }, + { label: 'Спорт', value: '15' }, + { label: 'Спортзалы', value: '1359' }, + { label: 'Сражения', value: '2868' }, + { label: 'Средневековье', value: '238' }, + { label: 'Ссоры / Конфликты', value: '645' }, + { label: 'Стаи', value: '2884' }, + { label: 'Сталкеры / Диггеры (субкультура)', value: '1029' }, + { label: 'Сталкинг', value: '605' }, + { label: 'Становление героя', value: '680' }, + { label: 'Стёб', value: '1690' }, + { label: 'Стереотипы', value: '809' }, + { label: 'Стимпанк', value: '25' }, + { label: 'Стимуляция руками', value: '225' }, + { label: 'Стихи', value: '1692' }, + { label: 'Стихийные бедствия', value: '1352' }, + { label: 'Стихотворные вставки', value: '2404' }, + { label: 'СтихоТворцы', value: '3015' }, + { label: 'Стокгольмский синдром / Лимский синдром', value: '1217' }, + { label: 'Столетняя война', value: '2243' }, + { label: 'Сторонний рассказчик', value: '1383' }, + { label: 'Стоунпанк', value: '1987' }, + { label: 'Страдания', value: '2009' }, + { label: 'Странногеддон', value: '1882' }, + { label: 'Страны Балтии', value: '1031' }, + { label: 'Страпоны', value: '380' }, + { label: 'Страсть', value: '1955' }, + { label: 'Страх перед родителями', value: '2332' }, + { label: 'Стрельба из лука', value: '1791' }, + { label: 'Стримеры / Ютуберы', value: '1473' }, + { label: 'Стрип-клубы', value: '79' }, + { label: 'Стрит-арт', value: '1012' }, + { label: 'Строители', value: '1774' }, + { label: 'Студенты', value: '274' }, + { label: 'Студенты по обмену', value: '859' }, + { label: 'Студенческие городки', value: '753' }, + { label: 'Субкультуры', value: '1061' }, + { label: 'Субординация', value: '1099' }, + { label: 'Суд', value: '827' }, + { label: 'Суккубы / Инкубы', value: '334' }, + { label: 'Сумасшествие', value: '1239' }, + { label: 'Сунагакуре', value: '2476' }, + { label: 'Супергерои', value: '67' }, + { label: 'Суперзлодеи', value: '1205' }, + { label: 'Супермаркеты', value: '843' }, + { label: 'Суррогатная беременность', value: '1427' }, + { label: 'Сухой закон', value: '2100' }, + { label: 'Сухой оргазм', value: '2818' }, + { label: 'Сфера ИТ', value: '2534' }, + { label: 'Сфинксы', value: '1349' }, + { label: 'Сценарий (стилизация)', value: '323' }, + { label: 'Счастливый финал', value: '3' }, + { label: 'США', value: '640' }, + { label: 'Съемочная площадка', value: '410' }, + { label: 'Сын Тьмы', value: '2175' }, + { label: 'Сэнгоку Джидай', value: '2380' }, + { label: 'Сэнгоку Дзидай', value: '2834' }, + { label: 'Сэцубун', value: '2969' }, + { label: 'Сюрреализм / Фантасмагория', value: '872' }, + { label: 'Т/И / Л/И', value: '167' }, + { label: 'Таджикистан', value: '2974' }, + { label: 'Таиланд', value: '1034' }, + { label: 'Тайна происхождения', value: '662' }, + { label: 'Тайная личность', value: '194' }, + { label: 'Тайная сущность', value: '1512' }, + { label: 'Тайны / Секреты', value: '906' }, + { label: 'Тайные друзья (игра)', value: '2908' }, + { label: 'Тайные организации', value: '572' }, + { label: 'Тайные поклонники', value: '1374' }, + { label: 'Тайный НапиСанта', value: '2972' }, + { label: 'Таксисты', value: '2679' }, + { label: 'Тактильный голод', value: '2197' }, + { label: 'Тактильный контакт', value: '2875' }, + { label: 'Танабата', value: '2340' }, + { label: 'Танцы', value: '108' }, + { label: 'Танцы на пилоне', value: '1370' }, + { label: 'Таро', value: '642' }, + { label: 'Тату-салоны', value: '324' }, + { label: 'Татуировки', value: '327' }, + { label: 'Твинцест', value: '1652' }, + { label: 'Творческий кризис', value: '2107' }, + { label: 'Театры', value: '195' }, + { label: 'Телекинез', value: '951' }, + { label: 'Телепатия', value: '127' }, + { label: 'Телепортация', value: '1794' }, + { label: 'Телесные жидкости', value: '1220' }, + { label: 'Телесные наказания', value: '132' }, + { label: 'Телесный хоррор', value: '212' }, + { label: 'Телохранители', value: '400' }, + { label: 'Темная сторона (Гарри Поттер)', value: '1937' }, + { label: 'Темная сторона Силы', value: '1946' }, + { label: 'Темная Эра (Bungou Stray Dogs)', value: '2494' }, + { label: 'Темное прошлое', value: '2229' }, + { label: 'Темное фэнтези', value: '1963' }, + { label: 'Темные властелины', value: '1224' }, + { label: 'Темные эльфы', value: '2597' }, + { label: 'Темный романтизм', value: '2128' }, + { label: 'Темы ментального здоровья', value: '2344' }, + { label: 'Темы этики и морали', value: '1106' }, + { label: 'Теннис', value: '1066' }, + { label: 'Тентакли', value: '129' }, + { label: 'Теория инверсии', value: '1980' }, + { label: 'Теория Одурманивания', value: '2493' }, + { label: 'Термокинез', value: '2793' }, + { label: 'Террористы', value: '556' }, + { label: 'Теслапанк', value: '1988' }, + { label: 'Техники', value: '1272' }, + { label: 'Техногенные катастрофы', value: '526' }, + { label: 'Технокинез', value: '1271' }, + { label: 'Технологии', value: '1232' }, + { label: 'Технотриллер', value: '649' }, + { label: 'Течка / Гон', value: '283' }, + { label: 'Тим Дрейк — Джокер-младший', value: '2454' }, + { label: 'Тим Дрейк — Кэтлэд', value: '2453' }, + { label: 'Тихий секс', value: '1185' }, + { label: 'Товарищи по несчастью', value: '2666' }, + { label: 'Токсичные родственники', value: '2008' }, + { label: 'Том Реддл — не Темный Лорд', value: '2394' }, + { label: 'Томбои', value: '565' }, + { label: 'Томгерлз', value: '848' }, + { label: 'Торговля органами', value: '1358' }, + { label: 'Торговцы', value: '2098' }, + { label: 'Торговые центры', value: '1416' }, + { label: 'Тоталитаризм', value: '1169' }, + { label: 'Травники / Травницы', value: '1211' }, + { label: 'Трагедия', value: '919' }, + { label: 'Трагикомедия', value: '1454' }, + { label: 'Традиции', value: '1463' }, + { label: 'Трансгендерные персонажи', value: '175' }, + { label: 'Трансплантация', value: '2364' }, + { label: 'Трансфобия', value: '420' }, + { label: 'Тревожное расстройство личности', value: '1914' }, + { label: 'Тревожность', value: '1318' }, + { label: 'Трейлеры', value: '785' }, + { label: 'Тренировки / Обучение', value: '876' }, + { label: 'Третий лишний', value: '1176' }, + { label: 'Третья мировая', value: '624' }, + { label: 'Три корейских государства', value: '2322' }, + { label: 'Трикстеры', value: '1507' }, + { label: 'Триллер', value: '169' }, + { label: 'Трилогия Юникрона', value: '2378' }, + { label: 'Трипофобия', value: '2342' }, + { label: 'Трисам', value: '758' }, + { label: 'Трихотилломания', value: '2185' }, + { label: 'Тролли', value: '190' }, + { label: 'Трудные отношения с родителями', value: '1490' }, + { label: 'Трудный характер', value: '2123' }, + { label: 'Трудоголизм', value: '1848' }, + { label: 'Трущобы', value: '2822' }, + { label: 'Туалетный юмор', value: '1247' }, + { label: 'Туберкулез', value: '2163' }, + { label: 'Турция', value: '794' }, + { label: 'Тщеславие', value: '2578' }, + { label: 'Тюрьмы / Темницы', value: '117' }, + { label: 'Тяжелое детство', value: '2614' }, + { label: 'Убийства', value: '621' }, + { label: 'Убийственная пара', value: '1918' }, + { label: 'Убийца поневоле', value: '2671' }, + { label: 'Уважение', value: '2688' }, + { label: 'Ужасы', value: '1679' }, + { label: 'Узбекистан', value: '1293' }, + { label: 'Уизлигады', value: '1862' }, + { label: 'Украина', value: '716' }, + { label: 'Укрытия', value: '199' }, + { label: 'Уличные артисты', value: '2550' }, + { label: 'Уличные гонки', value: '880' }, + { label: 'Умбракинез', value: '2355' }, + { label: 'Универсалы', value: '657' }, + { label: 'Унижения', value: '1319' }, + { label: 'Упоминания аддикций', value: '2848' }, + { label: 'Упоминания алкоголя', value: '2415' }, + { label: 'Упоминания беременности', value: '2057' }, + { label: 'Упоминания войны', value: '2707' }, + { label: 'Упоминания жестокости', value: '2447' }, + { label: 'Упоминания зоофилии', value: '2620' }, + { label: 'Упоминания измены', value: '2876' }, + { label: 'Упоминания изнасилования', value: '521' }, + { label: 'Упоминания инвалидности', value: '2705' }, + { label: 'Упоминания инцеста', value: '2285' }, + { label: 'Упоминания каннибализма', value: '2427' }, + { label: 'Упоминания курения', value: '2450' }, + { label: 'Упоминания мужской беременности', value: '2226' }, + { label: 'Упоминания наркотиков', value: '144' }, + { label: 'Упоминания насилия', value: '2207' }, + { label: 'Упоминания нездоровых отношений', value: '2892' }, + { label: 'Упоминания проституции', value: '2618' }, + { label: 'Упоминания пыток', value: '2410' }, + { label: 'Упоминания расизма', value: '2737' }, + { label: 'Упоминания религии', value: '1048' }, + { label: 'Упоминания самоубийства', value: '77' }, + { label: 'Упоминания секса', value: '2747' }, + { label: 'Упоминания селфхарма', value: '2105' }, + { label: 'Упоминания смертей', value: '2081' }, + { label: 'Упоминания смертей животных', value: '2211' }, + { label: 'Упоминания телесного хоррора', value: '2466' }, + { label: 'Упоминания терроризма', value: '774' }, + { label: 'Упоминания убийств', value: '1501' }, + { label: 'Управление оргазмом', value: '118' }, + { label: 'Управление стихиями', value: '2091' }, + { label: 'Уретральное проникновение', value: '1469' }, + { label: 'Уро-кинк', value: '762' }, + { label: 'Условное бессмертие', value: '2117' }, + { label: 'Уся / Сянься', value: '2714' }, + { label: 'Утопия', value: '350' }, + { label: 'Утраченная тройка', value: '2535' }, + { label: 'Утренний секс', value: '2882' }, + { label: 'Уход за персонажами с инвалидностью', value: '2133' }, + { label: 'Ухудшение отношений', value: '1219' }, + { label: 'Учебные заведения', value: '1694' }, + { label: 'Ученые', value: '402' }, + { label: 'Уют', value: '2738' }, + { label: 'Фавориты', value: '2135' }, + { label: 'Фамильяры', value: '1045' }, + { label: 'Фан-Бинго', value: '2957' }, + { label: 'Фан-Фанты', value: '2953' }, + { label: 'Фандомная аналитика', value: '742' }, + { label: 'Фандомная Битва', value: '253' }, + { label: 'Фансервис', value: '2483' }, + { label: 'Фантастика', value: '1670' }, + { label: 'Фастберн', value: '2016' }, + { label: 'Фастфуд / Закусочные', value: '1025' }, + { label: 'Феи', value: '191' }, + { label: 'Фельчинг', value: '2368' }, + { label: 'Фемдом', value: '54' }, + { label: 'Феминистские темы и мотивы', value: '523' }, + { label: 'Феминитивы', value: '390' }, + { label: 'Фениксы', value: '1215' }, + { label: 'Феодализм', value: '1504' }, + { label: 'Фермы / Ранчо', value: '329' }, + { label: 'Фестивали', value: '1006' }, + { label: 'Фехтование', value: '1775' }, + { label: 'Фиггинг', value: '1439' }, + { label: 'Фигурное катание', value: '460' }, + { label: 'Физиотерапия', value: '2222' }, + { label: 'Физическая сверхсила', value: '2880' }, + { label: 'Фикрайтеры / Фикридеры', value: '1284' }, + { label: 'Фиксированная раскладка', value: '1344' }, + { label: 'Фиктивные отношения', value: '308' }, + { label: 'Фиктивный брак', value: '34' }, + { label: 'Философия', value: '1675' }, + { label: 'Финляндия', value: '1133' }, + { label: 'Фистинг', value: '151' }, + { label: 'Флафф', value: '1667' }, + { label: 'Флирт', value: '349' }, + { label: 'Флот', value: '846' }, + { label: 'Фобии', value: '977' }, + { label: 'Фольклор и предания', value: '1273' }, + { label: 'Форнифилия', value: '580' }, + { label: 'Фотографы', value: '348' }, + { label: 'Фотокинез', value: '1805' }, + { label: 'Франция', value: '691' }, + { label: 'Фред Уизли жив', value: '2941' }, + { label: 'Фроттаж', value: '96' }, + { label: 'Фуд-плей', value: '509' }, + { label: 'Фуд-фетиш', value: '2168' }, + { label: 'Фурри', value: '996' }, + { label: 'Фут-фетиш', value: '432' }, + { label: 'Футбол', value: '458' }, + { label: 'Фэйри', value: '2028' }, + { label: 'Фэнтези', value: '1669' }, + { label: 'Хакеры', value: '340' }, + { label: 'Ханами', value: '1155' }, + { label: 'Ханахаки', value: '45' }, + { label: 'Ханжество', value: '882' }, + { label: 'Характерная для канона жестокость', value: '896' }, + { label: 'Харассмент', value: '477' }, + { label: 'Хиерофилия', value: '2366' }, + { label: 'Химеры', value: '816' }, + { label: 'Химическое оружие', value: '1186' }, + { label: 'Химэдэрэ / Оджидэрэ', value: '1363' }, + { label: 'Хип-хоп (субкультура)', value: '1822' }, + { label: 'Хиппи', value: '784' }, + { label: 'Хиромантия', value: '2749' }, + { label: 'Хирургическая коррекция пола', value: '1249' }, + { label: 'Хирургические операции', value: '1845' }, + { label: 'Хобби', value: '1161' }, + { label: 'Хогсмид', value: '2388' }, + { label: 'Хоккей', value: '828' }, + { label: 'Холодная война', value: '2140' }, + { label: 'Холодное оружие', value: '1187' }, + { label: 'Хорватия', value: '1433' }, + { label: 'Хороший плохой финал', value: '1266' }, + { label: 'Хронические заболевания', value: '1267' }, + { label: 'Хронокинез', value: '1447' }, + { label: 'Хронофантастика', value: '56' }, + { label: 'Хтонические существа', value: '504' }, + { label: 'Художники', value: '140' }, + { label: 'Хуманизация', value: '177' }, + { label: 'Хэллоуин', value: '205' }, + { label: 'Хюльдры', value: '1397' }, + { label: 'Цветочные магазины', value: '164' }, + { label: 'Целители', value: '1107' }, + { label: 'Церкви', value: '533' }, + { label: 'Цикличность', value: '1979' }, + { label: 'Цирки', value: '259' }, + { label: 'Цукими', value: '1798' }, + { label: 'Цундэрэ', value: '1137' }, + { label: 'Цыгане', value: '1091' }, + { label: 'Чайлдфри', value: '592' }, + { label: 'Чайные церемонии', value: '2416' }, + { label: 'Частичный ООС', value: '1409' }, + { label: 'Частные детективы', value: '427' }, + { label: 'Черная археология', value: '2503' }, + { label: 'Черная мораль', value: '2438' }, + { label: 'Чернобыльская катастрофа', value: '775' }, + { label: 'Черный юмор', value: '66' }, + { label: 'Чехия', value: '1021' }, + { label: 'Чиновники', value: '1991' }, + { label: 'Чирлидинг', value: '1431' }, + { label: 'Чистокровные AU (Гарри Поттер)', value: '1905' }, + { label: 'Чосон', value: '571' }, + { label: 'Чувство вины', value: '902' }, + { label: 'Чумные доктора', value: '1125' }, + { label: 'Чхусок', value: '2973' }, + { label: 'Шаманы', value: '484' }, + { label: 'Шантаж', value: '569' }, + { label: 'Шармбатон', value: '2733' }, + { label: 'Швейцария', value: '1022' }, + { label: 'Шейпшифтеры', value: '29' }, + { label: 'Шелки', value: '2431' }, + { label: 'Шифры', value: '2232' }, + { label: 'Школьная иерархия', value: '2787' }, + { label: 'Школьники', value: '2566' }, + { label: 'Школьные годы Тома Реддла', value: '1913' }, + { label: 'Школьные ОтМетки', value: '3017' }, + { label: 'Школьные поездки', value: '2481' }, + { label: 'Школьный роман', value: '2440' }, + { label: 'Шопинг', value: '864' }, + { label: 'Шоу-бизнес', value: '17' }, + { label: 'Шпионы', value: '148' }, + { label: 'Шрамирование', value: '891' }, + { label: 'Шрамы', value: '328' }, + { label: 'Эвтаназия', value: '1102' }, + { label: 'Эгоизм', value: '2071' }, + { label: 'Эдвардианская эпоха', value: '2975' }, + { label: 'Эзотерические темы и мотивы', value: '2801' }, + { label: 'Эйблизм', value: '453' }, + { label: 'Эйдетическая память', value: '975' }, + { label: 'Эйфория', value: '2458' }, + { label: 'Экзамены', value: '2591' }, + { label: 'Экзистенциальный кризис', value: '1200' }, + { label: 'Экзорцизм', value: '494' }, + { label: 'Экологи / Биологи', value: '631' }, + { label: 'Экологический кризис', value: '485' }, + { label: 'Экономический кризис', value: '1547' }, + { label: 'Эксаудиризм', value: '2151' }, + { label: 'Эксгибиционизм', value: '489' }, + { label: 'Эксперимент', value: '1686' }, + { label: 'Экстрасенсы', value: '371' }, + { label: 'Экшн', value: '1673' }, + { label: 'Электрокинез', value: '1857' }, + { label: 'Электростимуляция', value: '807' }, + { label: 'Элементы ангста', value: '2696' }, + { label: 'Элементы гета', value: '1714' }, + { label: 'Элементы дарка', value: '2703' }, + { label: 'Элементы детектива', value: '2698' }, + { label: 'Элементы драмы', value: '2697' }, + { label: 'Элементы других видов отношений', value: '454' }, + { label: 'Элементы мистики', value: '1324' }, + { label: 'Элементы психологии', value: '2824' }, + { label: 'Элементы романтики', value: '2742' }, + { label: 'Элементы слэша', value: '1715' }, + { label: 'Элементы ужасов', value: '2721' }, + { label: 'Элементы фемслэша', value: '1716' }, + { label: 'Элементы флаффа', value: '2731' }, + { label: 'Элементы юмора / Элементы стёба', value: '2011' }, + { label: 'Эльфы', value: '44' }, + { label: 'Эметофилия', value: '1434' }, + { label: 'Эмо', value: '641' }, + { label: 'Эмоциональная одержимость', value: '1270' }, + { label: 'Эмпатия', value: '1177' }, + { label: 'Энергетический вампиризм', value: '818' }, + { label: 'Эпидемии', value: '619' }, + { label: 'Эпизод 23.5', value: '2469' }, + { label: 'Эпилепсия', value: '1511' }, + { label: 'Эпилог? Какой эпилог?', value: '2651' }, + { label: 'Эпоха Бакумацу', value: '1472' }, + { label: 'Эпоха Мэйдзи', value: '1435' }, + { label: 'Эпоха Тайсё', value: '2015' }, + { label: 'Эрик Киллмонгер жив', value: '2897' }, + { label: 'Эрогуро', value: '1731' }, + { label: 'Эротическая дрессировка', value: '2026' }, + { label: 'Эротическая лактация', value: '284' }, + { label: 'Эротическая мумификация', value: '415' }, + { label: 'Эротическая сверхстимуляция', value: '1245' }, + { label: 'Эротические наказания', value: '125' }, + { label: 'Эротические ролевые игры', value: '111' }, + { label: 'Эротические сны', value: '128' }, + { label: 'Эротические фантазии', value: '561' }, + { label: 'Эротический массаж', value: '1518' }, + { label: 'Эротический перенос', value: '744' }, + { label: 'Эротомания', value: '2922' }, + { label: 'Эскапизм', value: '1552' }, + { label: 'Эскорты', value: '922' }, + { label: 'Эстетика', value: '2021' }, + { label: 'ЭТЕРНА', value: '2948' }, + { label: 'Этнические анклавы', value: '2732' }, + { label: 'Этническое фэнтези', value: '667' }, + { label: 'Эффект бабочки', value: '1572' }, + { label: 'Ювелиры', value: '2964' }, + { label: 'Южная Америка', value: '832' }, + { label: 'Южная готика', value: '1077' }, + { label: 'Южная Корея', value: '674' }, + { label: 'Юмор', value: '1666' }, + { label: 'Юристы', value: '333' }, + { label: 'Я никогда не... (игра)', value: '1436' }, + { label: 'Явное согласие', value: '1569' }, + { label: 'Ядерная война', value: '889' }, + { label: 'Язык жестов', value: '2962' }, + { label: 'Язык животных', value: '1327' }, + { label: 'Язык растений', value: '2622' }, + { label: 'Язык цветов', value: '272' }, + { label: 'Ямадад', value: '2916' }, + { label: 'Янгирэ', value: '1257' }, + { label: 'Яндэрэ', value: '1136' }, + { label: 'Яогуаи', value: '2347' }, + { label: 'Япония', value: '665' }, + { label: 'Японские фестивали', value: '2555' }, + { label: 'Ящеролюди', value: '682' }, + { label: 'Abuse/Comfort', value: '2599' }, + { label: 'Aged down', value: '229' }, + { label: 'Aged up', value: '4' }, + { label: 'Artfic', value: '68' }, + { label: 'AU', value: '1683' }, + { label: 'AU: Альтернативные способности', value: '2086' }, + { label: 'AU: Без апокалипсиса', value: '2051' }, + { label: 'AU: Без войны', value: '2305' }, + { label: 'AU: Без магии', value: '1052' }, + { label: 'AU: Без мистики', value: '2489' }, + { label: 'AU: Без сверхспособностей', value: '1053' }, + { label: 'AU: Без супергероев', value: '1152' }, + { label: 'AU: Все гражданские', value: '2797' }, + { label: 'AU: Все друзья', value: '2552' }, + { label: 'AU: Все люди', value: '1151' }, + { label: 'AU: Все хорошо', value: '2124' }, + { label: 'AU: Другая страна', value: '2165' }, + { label: 'AU: Другая эпоха', value: '2452' }, + { label: 'AU: Другое детство', value: '2360' }, + { label: 'AU: Другое знакомство', value: '2217' }, + { label: 'AU: Другое семейное положение', value: '2902' }, + { label: 'AU: Не родственники', value: '2167' }, + { label: 'AU: Профессиональные герои (BNHA)', value: '2924' }, + { label: 'AU: Родственники', value: '2164' }, + { label: 'AU: Школа', value: '2532' }, + { label: 'AU: Age swap', value: '917' }, + { label: 'AU: Kitty Gang', value: '2928' }, + { label: 'AU: Race swap', value: '933' }, + { label: 'AU: Reverse', value: '1156' }, + { label: 'AU: Role swap', value: '1054' }, + { label: 'Authors Time', value: '2959' }, + { label: 'BDSM', value: '1646' }, + { label: 'BDSM-вселенная', value: '1078' }, + { label: 'BDSM: Домспейс', value: '1444' }, + { label: 'BDSM: Дроп', value: '1411' }, + { label: 'BDSM: Сабспейс', value: '1410' }, + { label: 'BDSM: Aftercare', value: '1523' }, + { label: 'BEAST (Bungou Stray Dogs)', value: '2457' }, + { label: 'Blackwatch', value: '2116' }, + { label: 'Body inflation', value: '389' }, + { label: 'Character study', value: '363' }, + { label: 'Cockwarming', value: '318' }, + { label: 'Dark academia', value: '2430' }, + { label: 'DDLG / DDLB / MDLG / MDLB', value: '1033' }, + { label: 'Dirty talk', value: '139' }, + { label: 'Doggy style', value: '1361' }, + { label: 'Dollification', value: '2780' }, + { label: 'Dramione Authors Tournament', value: '2950' }, + { label: 'EIQ', value: '271' }, + { label: 'ER', value: '1687' }, + { label: 'Final Haikyuu Quest', value: '2652' }, + { label: 'Fix-it', value: '1' }, + { label: 'Frenemies', value: '666' }, + { label: 'Gangbang', value: '315' }, + { label: 'Glory hole', value: '2188' }, + { label: 'Golden Age Overwatch', value: '2112' }, + { label: 'GOLockdown', value: '2391' }, + { label: 'Gun play', value: '252' }, + { label: 'Hurt/Comfort', value: '1682' }, + { label: 'Hydra!AU', value: '1931' }, + { label: 'Ironqrow week', value: '2209' }, + { label: 'Kimetsu Gakuen', value: '2574' }, + { label: 'Knife play', value: '152' }, + { label: 'KPOP-AUFEST', value: '1851' }, + { label: 'LARP', value: '1510' }, + { label: 'LOVE ОтМетки', value: '2981' }, + { label: 'Maleval week', value: '2382' }, + { label: 'Marvel ГФИ', value: '2896' }, + { label: 'Mate or die', value: '375' }, + { label: 'Mirror!verse (Звездный путь)', value: '2899' }, + { label: 'Moresome', value: '759' }, + { label: 'N раз', value: '263' }, + { label: 'Omnic Crisis Era', value: '2111' }, + { label: "ORIGINAL'S 2021", value: '2939' }, + { label: 'Parentlock', value: '2053' }, + { label: 'Patronus of Dramione', value: '2958' }, + { label: 'Post-Fall of Overwatch', value: '2114' }, + { label: 'Post-Recall', value: '2115' }, + { label: 'PWP', value: '1680' }, + { label: 'Rape/Revenge', value: '2195' }, + { label: 'Reverse Falls', value: '1879' }, + { label: 'RofR ДраМиВояж', value: '2954' }, + { label: 'RST', value: '815' }, + { label: 'RTFM', value: '2327' }, + { label: 'Service top / Power bottom', value: '563' }, + { label: 'Shingeki no Kyojin High School AU', value: '2037' }, + { label: 'Shingeki! Kyojin Chuugakkou', value: '2038' }, + { label: 'Sickfic', value: '422' }, + { label: 'Songfic', value: '1684' }, + { label: 'Spider-son & Iron-dad', value: '1885' }, + { label: 'Sugar daddy', value: '106' }, + { label: 'Sugar mommy', value: '160' }, + { label: 'Superfamily (Мстители)', value: '2623' }, + { label: 'Superior Iron Man', value: '2676' }, + { label: 'Talon (Overwatch)', value: '2110' }, + { label: 'The Big Four / The Super Six', value: '1954' }, + { label: 'Trench AU', value: '2387' }, + { label: 'TYL', value: '1933' }, + { label: 'Underage', value: '1658' }, + { label: 'URT', value: '2761' }, + { label: 'UST', value: '1709' }, + { label: 'Versefic', value: '2729' }, + { label: 'Videofic', value: '2480' }, + { label: 'Womance', value: '265' }, + { label: 'X век', value: '240' }, + { label: 'XI век', value: '241' }, + { label: 'XII век', value: '242' }, + { label: 'XIII век', value: '243' }, + { label: 'XIV век', value: '244' }, + { label: 'XIX век', value: '249' }, + { label: 'XV век', value: '245' }, + { label: 'XVI век', value: '246' }, + { label: 'XVII век', value: '247' }, + { label: 'XVIII век', value: '248' }, + { label: 'XX век', value: '1004' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new ficbook(); + +type Data = { + data?: DataEntity[] | null; + more: boolean; +}; +type DataEntity = { + id: string; + slug: string; + author_id: number; + user_slug: string; + author_username: string; + real_author_id: string | null; + direction: number; + part_cnt: number; + title: string; + description: string; + enable_marks: boolean; + marks_plus: number; + is_premium: boolean; + status: number; + last_updated: string; + readedDate?: string | null; + reward_cnt: number; + cover?: string | null; +}; diff --git a/plugins/russian/jaomix.ts b/plugins/russian/jaomix.ts new file mode 100644 index 000000000..56c814c18 --- /dev/null +++ b/plugins/russian/jaomix.ts @@ -0,0 +1,328 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML, CheerioAPI } from 'cheerio'; +import dayjs from 'dayjs'; + +class Jaomix implements Plugin.PagePlugin { + id = 'jaomix.ru'; + name = 'Jaomix'; + site = 'https://jaomix.ru'; + version = '1.0.3'; + icon = 'src/ru/jaomix/icon.png'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/?searchrn'; + + if (filters?.lang?.value?.length) { + url += filters.lang.value + .map((lang, idx) => `&lang[${idx}]=${lang}`) + .join(''); + } + + if (filters?.genre?.value?.include?.length) { + url += filters.genre.value.include + .map((genre, idx) => `&genre[${idx}]=${genre}`) + .join(''); + } + + if (filters?.genre?.value?.exclude?.length) { + url += filters.genre.value.exclude + .map((genre, idx) => `&delgenre[${idx}]=del ${genre}`) + .join(''); + } + + url += '&sortcountchapt=' + (filters?.sortcountchapt?.value || '1'); + url += '&sortdaycreate=' + (filters?.sortdaycreate?.value || '1'); + url += + '&sortby=' + + (showLatestNovels ? 'upd' : filters?.sortby?.value || 'topweek'); + url += '&gpage=' + pageNo; + + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div[class="block-home"] > div[class="one"]').each( + (index, element) => { + const name = loadedCheerio(element) + .find('div[class="img-home"] > a') + .attr('title'); + const cover = loadedCheerio(element) + .find('div[class="img-home"] > a > img') + .attr('src') + ?.replace('-150x150', ''); + const url = loadedCheerio(element) + .find('div[class="img-home"] > a') + .attr('href'); + + if (!name || !url) return; + + novels.push({ name, cover, path: url.replace(this.site, '') }); + }, + ); + + return novels; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('div[class="desc-book"] > h1').text().trim(), + cover: loadedCheerio('div[class="img-book"] > img').attr('src'), + summary: loadedCheerio('div[id="desc-tab"]').text().trim(), + totalPages: loadedCheerio('.sel-toc > option').length, + }; + + loadedCheerio('#info-book > p').each(function () { + const text = loadedCheerio(this).text().replace(/,/g, '').split(' '); + if (text[0] === 'Автор:') { + novel.author = text.splice(1).join(' '); + } else if (text[0] === 'Жанры:') { + novel.genres = text.splice(1).join(','); + } else if (text[0] === 'Статус:') { + novel.status = text.includes('продолжается') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + } + }); + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const body = await fetchApi(`${this.site}/wp-admin/admin-ajax.php`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + novelPath, + Origin: this.site, + }, + body: new URLSearchParams({ + action: 'loadpagenavchapstt', + page, + }).toString(), + }).then(data => data.text()); + + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + + return { + chapters, + }; + } + + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('div.title').each((_, element) => { + const name = loadedCheerio(element).find('a').attr('title'); + const url = loadedCheerio(element).find('a').attr('href'); + if (!name || !url) return; + + const releaseDate = loadedCheerio(element).find('time').text(); + chapters.push({ + name, + path: url.replace(this.site, ''), + releaseTime: this.parseDate(releaseDate), + }); + }); + + return chapters; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + loadedCheerio('.adblock-service, .lazyblock').remove(); + const chapterText = loadedCheerio('.entry-content').html() || ''; + + return chapterText.replace(/<a[^>]*>(.*?)<\/a>/gi, '$1'); + } + + async searchNovels( + searchTerm: string, + pageNo: number | undefined = 1, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + + '/?searchrn=' + + searchTerm + + '&but=Поиск по названию&sortby=upd&gpage=' + + pageNo; + const body = await fetchApi(url).then(res => res.text()); + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + loadedCheerio('div[class="block-home"] > div[class="one"]').each( + (index, element) => { + const name = loadedCheerio(element) + .find('div[class="img-home"] > a') + .attr('title'); + const cover = loadedCheerio(element) + .find('div[class="img-home"] > a > img') + .attr('src') + ?.replace('-150x150', ''); + const url = loadedCheerio(element) + .find('div[class="img-home"] > a') + .attr('href'); + + if (!name || !url) return; + + novels.push({ name, cover, path: url.replace(this.site, '') }); + }, + ); + + return novels; + } + + parseDate = (dateString: string | undefined = '') => { + const months: Record<string, number> = { + Янв: 1, + Фев: 2, + Мар: 3, + Апр: 4, + Май: 5, + Июн: 6, + Июл: 7, + Авг: 8, + Сен: 9, + Окт: 10, + Ноя: 11, + Дек: 12, + }; + + const [time, day, month, year] = dateString.split(' '); + if (time && day && months[month] && year) { + return dayjs(year + '-' + months[month] + '-' + day + ' ' + time).format( + 'LLL', + ); + } + + return dateString || null; + }; + + filters = { + sortby: { + label: 'Сортировка:', + value: 'topweek', + options: [ + { label: 'Топ недели', value: 'topweek' }, + { label: 'По алфавиту', value: 'alphabet' }, + { label: 'По дате обновления', value: 'upd' }, + { label: 'По дате создания', value: 'new' }, + { label: 'По просмотрам', value: 'count' }, + { label: 'Топ года', value: 'topyear' }, + { label: 'Топ дня', value: 'topday' }, + { label: 'Топ за все время', value: 'alltime' }, + { label: 'Топ месяца', value: 'topmonth' }, + ], + type: FilterTypes.Picker, + }, + sortdaycreate: { + label: 'Дата добавления:', + value: '1', + options: [ + { label: 'Дата добавления', value: '1' }, + { label: 'От 120 до 180 дней', value: '1218' }, + { label: 'От 180 до 365 дней', value: '1836' }, + { label: 'От 30 до 60 дней', value: '3060' }, + { label: 'От 365 дней', value: '365' }, + { label: 'От 60 до 90 дней', value: '6090' }, + { label: 'От 90 до 120 дней', value: '9012' }, + { label: 'Послед. 30 дней', value: '30' }, + ], + type: FilterTypes.Picker, + }, + sortcountchapt: { + label: 'Количество глав:', + value: '1', + options: [ + { label: 'Любое кол-во глав', value: '1' }, + { label: 'До 500', value: '500' }, + { label: 'От 1000 до 2000', value: '1020' }, + { label: 'От 2000 до 3000', value: '2030' }, + { label: 'От 3000 до 4000', value: '3040' }, + { label: 'От 4000', value: '400' }, + { label: 'От 500 до 1000', value: '510' }, + ], + type: FilterTypes.Picker, + }, + genre: { + label: 'Жанры:', + value: { include: [], exclude: [] }, + options: [ + { label: 'Боевые Искусства', value: 'Боевые Искусства' }, + { label: 'Виртуальный Мир', value: 'Виртуальный Мир' }, + { label: 'Гарем', value: 'Гарем' }, + { label: 'Детектив', value: 'Детектив' }, + { label: 'Драма', value: 'Драма' }, + { label: 'Игра', value: 'Игра' }, + { label: 'Истории из жизни', value: 'Истории из жизни' }, + { label: 'Исторический', value: 'Исторический' }, + { label: 'История', value: 'История' }, + { label: 'Исэкай', value: 'Исэкай' }, + { label: 'Комедия', value: 'Комедия' }, + { label: 'Меха', value: 'Меха' }, + { label: 'Мистика', value: 'Мистика' }, + { label: 'Научная Фантастика', value: 'Научная Фантастика' }, + { label: 'Повседневность', value: 'Повседневность' }, + { label: 'Постапокалипсис', value: 'Постапокалипсис' }, + { label: 'Приключения', value: 'Приключения' }, + { label: 'Психология', value: 'Психология' }, + { label: 'Романтика', value: 'Романтика' }, + { label: 'Сверхъестественное', value: 'Сверхъестественное' }, + { label: 'Сёнэн', value: 'Сёнэн' }, + { label: 'Спорт', value: 'Спорт' }, + { label: 'Сэйнэн', value: 'Сэйнэн' }, + { label: 'Сюаньхуа', value: 'Сюаньхуа' }, + { label: 'Трагедия', value: 'Трагедия' }, + { label: 'Триллер', value: 'Триллер' }, + { label: 'Фантастика', value: 'Фантастика' }, + { label: 'Фэнтези', value: 'Фэнтези' }, + { label: 'Хоррор', value: 'Хоррор' }, + { label: 'Школьная жизнь', value: 'Школьная жизнь' }, + { label: 'Шоунен', value: 'Шоунен' }, + { label: 'Экшн', value: 'Экшн' }, + { label: 'Этти', value: 'Этти' }, + { label: 'Adult', value: 'Adult' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Mature', value: 'Mature' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Wuxia', value: 'Wuxia' }, + { label: 'Xianxia', value: 'Xianxia' }, + { label: 'Xuanhuan', value: 'Xuanhuan' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + lang: { + label: 'Выбрать языки:', + value: [], + options: [ + { label: 'Английский', value: 'Английский' }, + { label: 'Китайский', value: 'Китайский' }, + { label: 'Корейский', value: 'Корейский' }, + { label: 'Японский', value: 'Японский' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new Jaomix(); diff --git a/plugins/russian/neobook.ts b/plugins/russian/neobook.ts new file mode 100644 index 000000000..fb43382c1 --- /dev/null +++ b/plugins/russian/neobook.ts @@ -0,0 +1,455 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; + +const statusKey: Record<string, string> = { + '0': NovelStatus.Unknown, + '1': NovelStatus.Ongoing, + '2': NovelStatus.Completed, + '3': NovelStatus.OnHiatus, + '4': NovelStatus.Cancelled, +}; + +class Neobook implements Plugin.PluginBase { + id = 'neobook'; + name = 'Neobook'; + site = 'https://neobook.org'; + apiSite = 'https://api.neobook.org/'; + version = '1.0.3'; + icon = 'src/ru/neobook/icon.png'; + + async fetchNovels( + page: number, + showLatestNovels?: boolean, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + searchTerm?: string, + ): Promise<Plugin.NovelItem[]> { + const formData = new FormData(); + formData.append('version', '4.4'); + formData.append('uid', '0'); + formData.append('utoken', ''); + formData.append('resource', 'general'); + formData.append('action', 'get_bundle'); + formData.append('bundle', 'bundle_books'); + formData.append('target', 'feed'); + formData.append('page', page.toString()); + formData.append('filter_category_id', filters?.category?.value || '0'); + formData.append('filter_completed', '-1'); + formData.append('filter_search', searchTerm || ''); + formData.append('filter_tags', filters?.tags?.value || ''); + formData.append( + 'filter_sort', + showLatestNovels ? 'new' : filters?.sort?.value || 'popular', + ); + formData.append('filter_timeread', filters?.timeread?.value || '0-999999'); + + const { bundle_books }: { bundle_books: BundleBooks } = await fetchApi( + this.apiSite, + { + method: 'post', + body: formData, + referrer: this.site, + }, + ).then(res => res.json()); + const novels: Plugin.NovelItem[] = []; + + if (bundle_books.feed?.length) { + bundle_books.feed?.forEach(novel => + novels.push({ + name: novel.title, + cover: novel?.attachment?.image?.m || defaultCover, + path: novel.token + '/', + }), + ); + } + + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + return this.fetchNovels(page, showLatestNovels, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, false, undefined, searchTerm); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + cover: defaultCover, + }; + + const bookRaw = body.match(/var postData = ({.*?});/); + if (bookRaw instanceof Array && bookRaw[1]) { + const book: Novels = JSON.parse(bookRaw[1]); + + novel.name = book.title; + novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix; + novel.author = + book.user && book.user.firstname && book.user.lastname + ? book.user.firstname + ' ' + book.user.lastname + : book.user?.initials || ''; + novel.status = statusKey[book.status || '0'] || NovelStatus.Unknown; + + if (book.attachment?.image?.m) novel.cover = book.attachment.image.m; + if (book.tags?.length) novel.genres = book.tags.join(','); + + const chapters: Plugin.ChapterItem[] = []; + + book.chapters?.forEach((chapter, chapterIndex) => { + if (chapter.access == '1' && chapter.status == '1') { + chapters.push({ + name: chapter.title || 'Глава ' + (chapterIndex + 1), + path: `?book=${book.token}&chapter=${chapter.token}`, + releaseTime: null, + chapterNumber: Number(chapter.sort) || chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + } + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); + + const bookRaw = body.match(/var data = ({.*?});/); + let chapterText = ''; + + if (bookRaw instanceof Array && bookRaw[1]) { + const token = chapterPath.split('=')[2]; + const book: Novels = JSON.parse(bookRaw[1]); + const chapter = book.chapters?.find?.(chapter => chapter.token == token); + chapterText = (chapter?.data?.html || '').replace(/<br>/g, ''); + } + + return chapterText; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/book/' : '/reader/') + path; + + filters = { + sort: { + label: 'Сортировка:', + value: 'popular', + options: [ + { label: 'Сначала популярные', value: 'popular' }, + { label: 'Сначала новые', value: 'new' }, + { label: 'В случайном порядке', value: 'rand' }, + ], + type: FilterTypes.Picker, + }, + timeread: { + label: 'Время прочтения:', + value: '0-999999', + options: [ + { label: 'Все', value: '0-999999' }, + { label: 'Менее 15 минут', value: '0-900' }, + { label: '15-30 минут', value: '900-1800' }, + { label: '30-60 минут', value: '1800-3600' }, + { label: '1-2 часа', value: '3600-7200' }, + { label: 'Более 2 часов', value: '7200-999999' }, + ], + type: FilterTypes.Picker, + }, + category: { + label: 'Жанр:', + value: '', + options: [ + { label: 'Все', value: '' }, + { label: 'Антиутопия', value: '10' }, + { label: 'Детектив', value: '13' }, + { label: 'Детские книги', value: '14' }, + { label: 'Драма', value: '15' }, + { label: 'Другое', value: '16' }, + { label: 'История', value: '18' }, + { label: 'Мелодрама', value: '21' }, + { label: 'Мистика', value: '22' }, + { label: 'Научная фантастика', value: '23' }, + { label: 'Нон-фикшн', value: '35' }, + { label: 'Подростки и молодежь', value: '26' }, + { label: 'Постапокалипсис', value: '27' }, + { label: 'Поэзия', value: '28' }, + { label: 'Приключения', value: '29' }, + { label: 'Рассказ', value: '34' }, + { label: 'Роман', value: '36' }, + { label: 'Творчество', value: '40' }, + { label: 'Триллер', value: '91' }, + { label: 'Ужасы', value: '90' }, + { label: 'Фантастика', value: '41' }, + { label: 'Фанфик', value: '42' }, + { label: 'Фэнтези', value: '44' }, + { label: 'Эротика', value: '46' }, + { label: 'Юмор', value: '47' }, + ], + type: FilterTypes.Picker, + }, + tags: { + label: 'Тэги:', + value: '', + type: FilterTypes.TextInput, + }, + } satisfies Filters; +} + +export default new Neobook(); + +type BundleBooks = { + // categories: any[]; + // topSection: any[]; + feed: Novels[]; +}; + +type BottomSection = { + id: string; + boost: string; + preferenceid: string; + type_id: string; + contentTypeid: string; + name: string; + title: string; + more: string; + part: string; + items: Novels[]; +}; + +type Novels = { + status?: string; + error?: string; + login?: Login; + requestid?: string; + bundleBooks?: BundleBooks; + available?: string; + boosted?: string; + id?: string; + token: string; + type?: string; + typeid?: string; + verified?: string; + audio?: string; + labelid?: string; + labelTitle?: string; + languageid?: string; + languageTitle?: string; + categoryid?: string; + categoryTitle?: string; + price?: Price; + priceSpecial?: Price; + priceDiscount?: string; + title: string; + text: string; + text_fix: string; + copyright?: string; + align?: string; + completed?: string; + timereadS?: string; + views?: string; + likes?: string; + comments?: string; + bookmarks?: string; + isLike?: string; + isBookmark?: string; + isPurchase?: string; + isSelf?: string; + dateEdit?: string; + datePublish?: string; + link_path?: string; + user?: NovelsUser; + attachment?: Attachment; + adult?: string; + adultPlaceholder?: string; + nft?: string; + feed?: Feed; + counters?: NovelsCounters; + rating?: Rating; + readTime?: ReadTime; + readProgress?: ReadProgress; + tags?: string[]; + chapters?: Chapter[]; + // lastComments?: any[]; + bottomSection?: BottomSection; + carouselItems?: Novels[]; + book?: Novels; + activeChapterIndex?: number; +}; + +type Attachment = { + has: string; + id: string; + typeid: string; + userid: string; + width: string; + height: string; + duration: string; + image: Image; + video: string; + origin: string; + ext: string; +}; + +type Image = { + s: string; + m: string; + l: string; + has?: string; +}; + +type Chapter = { + id: string; + token: string; + status: string; + sort: string; + free: string; + title: string; + words: string; + access: string; + loaded: string; + data?: Data; +}; + +type Data = { + available: string; + access: string; + id: string; + token: string; + postid: string; + free: string; + title: string; + html: string; + dateEdit: string; + datePublish: string; + attachment: Attachment; + // lastComments: any[]; +}; + +type NovelsCounters = { + impressions: Bookmarks; + views: Bookmarks; + likes: Bookmarks; + comments: Bookmarks; + bookmarks: Bookmarks; +}; + +type Bookmarks = { + value: string; + formatted: string; +}; + +type Feed = { + name: string; + canRemoved: string; +}; + +type Login = { + uid: string; + utoken: string; + has: string; + banned: string; + verified: string; + acc: string; + arm: string; + grm: string; + counters: LoginCounters; + user: LoginUser; + // pro: any[]; + // earn: any[]; + // boost: any[]; + // deposit: any[]; + // affiliate: any[]; + // preferences: any[]; +}; + +type LoginCounters = { + notifications: string; + messages: string; +}; + +type LoginUser = { + id: string; + username: string; + firstname: string; + lastname: string; + photo: string; + status: string; + balance: Balance; + connectedService: string; +}; + +type Balance = { + neo: Price; +}; + +type Price = { + has: string; + value: string; + currency: Currency; + formatted: string; + deadline: string; +}; + +type Currency = 'NEO'; + +type Rating = { + value: string; + count: string; + my: string; +}; + +type ReadProgress = { + chapter: string; + progress: string; +}; + +type ReadTime = { + hours: string; + minutes: string; +}; + +type NovelsUser = { + available: string; + boosted: string; + id: string; + status: string; + verified: string; + metrica: string; + cover: Image; + photo: Image; + username: string; + firstname: string; + lastname: string; + initials: string; + link: string; + about: string; + subscriptions: string; + subscribers: string; + publications: string; + books: string; + poems: string; + posts: string; + drafts: string; + isPro: string; + isAdult: string; + isBanned: string; + isSubscription: string; + isBlock: string; + gr: string; +}; diff --git a/plugins/russian/novelOvh.broken.ts b/plugins/russian/novelOvh.broken.ts new file mode 100644 index 000000000..f70f6136f --- /dev/null +++ b/plugins/russian/novelOvh.broken.ts @@ -0,0 +1,485 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class novelOvh implements Plugin.PluginBase { + id = 'novelovh'; + name = 'НовелОВХ'; + site = 'https://novel.ovh'; + apiSite = 'https://api.novel.ovh/v2/'; + version = '1.0.3'; + icon = 'src/ru/novelovh/icon.png'; + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + let url = this.apiSite + 'books?page=' + (pageNo - 1); + url += + '&sort=' + + (showLatestNovels + ? 'updatedAt' + : filters?.sort?.value || 'averageRating') + + ',desc'; + + const books: BooksEntity[] = await fetchApi(url).then((res: Response) => + res.json(), + ); + + const novels: Plugin.NovelItem[] = []; + books.forEach(novel => + novels.push({ + name: novel.name.ru, + cover: novel.poster, + path: novel.slug, + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const { book, branches, chapters }: responseNovel = await fetchApi( + this.site + + '/content/' + + novelPath + + '?_data=routes/reader/book/$slug/index', + ).then((res: Response) => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: book.name.ru, + cover: book.poster, + genres: book.labels?.map?.(label => label.name).join(','), + summary: book.description, + status: + book.status == 'ONGOING' ? NovelStatus.Ongoing : NovelStatus.Completed, + }; + + book.relations?.forEach(person => { + switch (person.type) { + case 'AUTHOR': + novel.author = person.publisher.name; + break; + case 'ARTIST': + novel.artist = person.publisher.name; + break; + } + }); + + const branch_name: Record<string, string> = {}; + if (branches.length) { + branches.forEach(({ id, publishers }) => { + if (id && publishers?.length) + branch_name[id] = publishers[0].name || 'Неизвестный'; + }); + } + + const chaptersRes: Plugin.ChapterItem[] = []; + chapters.forEach((chapter, chapterIndex) => + chaptersRes.push({ + name: + chapter.title || + 'Том ' + + (chapter.volume || 0) + + ' ' + + (chapter.name || + 'Глава ' + (chapter.number || chapters.length - chapterIndex)), + path: novelPath + '/' + chapter.id, + releaseTime: dayjs(chapter.createdAt).format('LLL'), + chapterNumber: chapters.length - chapterIndex, + page: branch_name[chapter.branchId] || 'Главная страница', + }), + ); + + novel.chapters = chaptersRes.reverse(); + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const book: responseChapter = await fetchApi( + this.apiSite + 'chapters/' + chapterPath.split('/')[1], + ).then((res: Response) => res.json()); + + const image = Object.fromEntries( + book?.pages?.map(({ id, image }) => [id, image]) || [], + ); + + const chapterText = this.jsonToHtml(book.content.content || [], image); + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = this.apiSite + 'books?type=NOVEL&search=' + searchTerm; + const books: BooksEntity[] = await fetchApi(url).then((res: Response) => + res.json(), + ); + + const novels: Plugin.NovelItem[] = []; + books.forEach(novel => + novels.push({ + name: novel.name.ru, + cover: novel.poster, + path: novel.slug, + }), + ); + + return novels; + } + + jsonToHtml = ( + json: ContentEntity[], + image: Record<string, string>, + html = '', + ) => { + json.forEach(element => { + switch (element.type) { + case 'image': + if (element.attrs?.pages?.[0]) { + html += `<img src="${image[element.attrs.pages[0]]}"/>`; + } + break; + case 'hardBreak': + html += '<br>'; + break; + case 'horizontalRule': + case 'delimiter': + html += '<h2 style="text-align: center">***</h2>'; + break; + case 'paragraph': + html += + '<p>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</p>'; + break; + case 'orderedList': + html += + '<ol>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</ol>'; + break; + case 'listItem': + html += + '<li>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</li>'; + break; + case 'blockquote': + html += + '<blockquote>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</blockquote>'; + break; + case 'italic': + html += + '<i>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</i>'; + break; + case 'bold': + html += + '<b>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</b>'; + break; + case 'underline': + html += + '<u>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</u>'; + break; + case 'heading': + html += + '<h2>' + + (element.content + ? this.jsonToHtml(element.content, image) + : '<br>') + + '</h2>'; + break; + case 'text': + html += element.text; + break; + default: + html += JSON.stringify(element, null, '\t'); //maybe I missed something. + break; + } + }); + return html; + }; + + resolveUrl = (path: string) => this.site + '/content/' + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'averageRating', + options: [ + { label: 'Кол-во просмотров', value: 'viewsCount' }, + { label: 'Кол-во лайков', value: 'likesCount' }, + { label: 'Кол-во глав', value: 'chaptersCount' }, + { label: 'Кол-во закладок', value: 'bookmarksCount' }, + { label: 'Рейтингу', value: 'averageRating' }, + { label: 'Дате создания', value: 'createdAt' }, + { label: 'Дате обновления', value: 'updatedAt' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new novelOvh(); + +type BooksEntity = { + id: string; + slug: string; + type: string; + serviceName: string; + poster: string; + background?: string | null; + featuredCharacter?: null; + featuredCharacterBackground?: null; + featuredCharacterAnimation?: null; + featuredCharacterAnimationWithMask?: null; + featuredCharacterAnimationFirstFrame?: null; + status: string; + contentStatus: string; + name: Name; + altNames?: AltNamesEntity[] | null; + country: string; + year: number; + formats?: null[] | null; + featured: boolean; + viewsCount: number; + likesCount: number; + bookmarksCount: number; + ratingVotesCount: number; + averageRating: number; + chaptersCount: number; + createdAt: string; + updatedAt: string; +}; +type Name = { + ru: string; + original: string; + en: string; +}; +type AltNamesEntity = { + language: string; + name: string; +}; + +type responseNovel = { + book: Book; + branches: BranchesEntity[]; + chapters: ChaptersEntity[]; + related?: RelatedEntityOrBook[] | null; + associated?: AssociatedEntity[] | null; + chapterLikes?: null[] | null; + covers?: CoversEntity[] | null; + promotionVideos?: null[] | null; + folderItems?: null[] | null; + contentNotices?: null[] | null; +}; +type Book = { + id: string; + slug: string; + type: string; + serviceName: string; + poster: string; + background?: null; + featuredCharacter?: null; + featuredCharacterBackground?: null; + featuredCharacterAnimation?: null; + featuredCharacterAnimationWithMask?: null; + featuredCharacterAnimationFirstFrame?: null; + contentBlockedCountries?: null[] | null; + status: string; + contentStatus: string; + moderationStatus: string; + name: Name; + altNames?: null[] | null; + externalLinks?: string[] | null; + description: string; + country: string; + year: number; + formats?: null[] | null; + featured: boolean; + viewsCount: number; + viewsDayCount: number; + viewsWeekCount: number; + viewsMonthCount: number; + likesCount: number; + bookmarksCount: number; + ratingVotesCount: number; + averageRating: number; + chaptersCount: number; + labels?: LabelsEntity[] | null; + relations?: RelationsEntity[] | null; + createdById: string; + createdAt: string; + updatedAt: string; + rating?: null; +}; +type LabelsEntity = { + id: string; + slug: string; + name: string; +}; +type RelationsEntity = { + type: string; + publisher: Publisher; +}; +type Publisher = { + id: string; + kind: string; + slug: string; + name: string; + altNames?: (AltNamesEntity | null)[] | null; + poster: string; + background?: null; + trusted: boolean; + donationsEnabled: boolean; + vkGroupId?: null; + vkGroupName?: null; + tonAddress?: null; + createdAt: string; + updatedAt: string; +}; +type BranchesEntity = { + id: string; + licensed: boolean; + editorsChoice: boolean; + chaptersCount: number; + createdAt: string; + updatedAt: string; + publishers?: PublishersEntity[] | null; + subscription?: null; + bookmark?: null; +}; +type PublishersEntity = { + id: string; + kind: string; + slug: string; + name: string; + altNames?: AltNamesEntity[] | null; + poster: string; + background?: null; + trusted: boolean; + donationsEnabled: boolean; + vk?: null; + tonAddress?: null; + createdAt: string; + updatedAt: string; +}; +type ChaptersEntity = { + id: string; + name?: string | null; + title?: string | null; + number: number; + volume: number; + likesCount: number; + commentsCount: number; + donut: boolean; + corrupted: boolean; + createdById: string; + branchId: string; + bookId: string; + createdAt: string; + expiredAt: string; + updatedAt: string; +}; +type RelatedEntityOrBook = { + id: string; + type: string; + slug: string; + poster: string; + motion?: null; + status: string; + contentStatus: string; + name: Name; + country: string; + year: number; + formats?: null[] | null; + viewsCount: number; + likesCount: number; + bookmarksCount: number; + ratingVotesCount: number; + averageRating: number; + createdAt: string; + updatedAt: string; +}; +type AssociatedEntity = { + id: string; + kind: string; + createdAt: string; + updatedAt: string; + book: RelatedEntityOrBook; +}; +type CoversEntity = { + id: string; + volume: number; + image: string; + createdAt: string; + updatedAt: string; +}; + +type responseChapter = { + id: string; + name?: string; + title: string; + number: number; + volume: number; + likesCount: number; + commentsCount: number; + moderationStatus: string; + content: Content; + pages?: PagesEntity[]; + donut: boolean; + corrupted: boolean; + createdById: string; + branchId: string; + bookId: string; + publishers?: PublishersEntity[] | null; + createdAt: string; + expiredAt: string; + updatedAt: string; +}; +type Content = { + type: string; + content?: ContentEntity[] | null; +}; +type ContentEntity = { + text?: string | null; + type: string; + attrs?: Attrs | null; + content?: Content[] | null; +}; +type Attrs = { + textAlign?: string; + pages?: [string]; +}; +type PagesEntity = { + id: string; + image: string; + index: number; + commentsCount: number; + height: number; + width: number; + createdAt: string; +}; diff --git a/plugins/russian/novelTL.ts b/plugins/russian/novelTL.ts new file mode 100644 index 000000000..73908cb3b --- /dev/null +++ b/plugins/russian/novelTL.ts @@ -0,0 +1,935 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +const statusKey: Record<string, string> = { + 'active': NovelStatus.Ongoing, + 'completed': NovelStatus.Completed, + 'freezed': NovelStatus.OnHiatus, + 'unknown': NovelStatus.Unknown, +}; + +class TL implements Plugin.PluginBase { + id = 'TL'; + name = 'NovelTL'; + site = 'https://novel.tl'; + version = '1.0.2'; + icon = 'src/ru/noveltl/icon.png'; + + async fetchNovels( + page: number, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + searchTerm?: string, + ): Promise<Plugin.NovelItem[]> { + const { data }: response = await fetchApi( + this.site + '/api/site/v2/graphql', + { + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', + }, + Referer: this.site, + body: JSON.stringify({ + query: + 'query Projects($hostname:String! $filter:SearchFilter $page:Int $limit:Int){projects(section:{fullUrl:$hostname}filter:$filter page:{pageSize:$limit,pageNumber:$page}){content{title fullUrl covers{url}}}}', + variables: { + filter: { + query: searchTerm || undefined, + tags: filters?.tags?.value?.length + ? filters.tags.value + : undefined, + genres: filters?.genres?.value?.length + ? filters.genres.value + : undefined, + }, + hostname: 'novel.tl', + limit: 40, + page, + }, + }), + }, + ).then(res => res.json()); + + const novels: Plugin.NovelItem[] = []; + data?.projects?.content?.forEach(novel => + novels.push({ + name: novel.title, + path: novel.fullUrl, + cover: novel?.covers?.[0]?.url + ? this.site + novel.covers[0].url + : defaultCover, + }), + ); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + return this.fetchNovels(page, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, undefined, searchTerm); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const { data }: response = await fetchApi( + this.site + '/api/site/v2/graphql', + { + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', + }, + Referer: this.site, + body: JSON.stringify({ + query: + 'query Book($url:String){project(project:{fullUrl:$url}){title translationStatus fullUrl covers{url}persons(langs:["ru","en","*"],roles:["author","illustrator"]){role name{firstName lastName}}genres{nameRu nameEng}tags{nameRu nameEng}annotation{text}subprojects{content{title volumes{content{shortName chapters{title publishDate fullUrl published}}}}}}}', + variables: { + url: novelPath, + }, + }), + }, + ).then(res => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: data.project?.title || '', + cover: data.project?.covers?.[0]?.url + ? this.site + data.project.covers[0].url + : defaultCover, + summary: data.project?.annotation?.text, + status: + statusKey[data.project?.translationStatus || 'unknown'] || + NovelStatus.Unknown, + }; + + const genres = [data.project?.tags, data.project?.genres] + .flat() + .map(item => item?.nameRu || item?.nameEng) + .filter(item => item); + + if (genres?.length) { + novel.genres = genres.join(', '); + } + + data.project?.persons?.forEach(person => { + if (person.role == 'author' && person.name.firstName) { + novel.author = + person.name.firstName + ' ' + (person.name?.lastName || ''); + } + if (person.role == 'illustrator' && person.name.firstName) { + novel.artist = + person.name.firstName + ' ' + (person.name?.lastName || ''); + } + }); + + const chapters: Plugin.ChapterItem[] = []; + + data.project?.subprojects?.content?.forEach(work => + work.volumes.content.forEach((volume, volumeIndex) => + volume.chapters.forEach((chapter, chapterIndex) => { + if (chapter.published) { + chapters.push({ + name: + (volume.shortName || 'Том ' + (volumeIndex + 1)) + + ' ' + + (chapter.title || 'Глава ' + (chapterIndex + 1)), + path: chapter.fullUrl, + releaseTime: dayjs(chapter.publishDate).format('LLL'), + chapterNumber: chapters.length + 1, + page: work.title || 'Основная серия', + }); + } + }), + ), + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const { data } = await fetchApi(this.site + '/api/site/v2/graphql', { + method: 'post', + headers: { + Accept: 'application/json, text/plain, */*', + 'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3', + 'Content-Type': 'application/json', + }, + Referer: this.site, + body: JSON.stringify({ + query: + 'query($url:String){chapter(chapter:{fullUrl:$url}){text{text}}}', + variables: { + url: decodeURI(chapterPath), + }, + }), + }).then(res => res.json()); + + const chapterText = data.chapter?.text?.text || ''; + return chapterText; + } + + resolveUrl = (path: string) => 'https://' + path; + + filters = { + tags: { + label: 'Тэги', + value: [], + options: [ + { label: 'Автоматы', value: '64' }, + { label: 'Агрессивные персонажи', value: '26' }, + { label: 'Ад', value: '338' }, + { label: 'Адаптация манги', value: '8' }, + { label: 'Академия', value: '5' }, + { label: 'Актёрское искусство', value: '7' }, + { label: 'Актеры озвучки', value: '746' }, + { label: 'Алхимия', value: '27' }, + { label: 'Альтернативная реальность', value: '30' }, + { label: 'Амнезия', value: '31' }, + { label: 'Анабиоз', value: '170' }, + { label: 'Анал', value: '33' }, + { label: 'Ангелы', value: '38' }, + { label: 'Андроиды', value: '37' }, + { label: 'Анти-рост персонажа', value: '25' }, + { label: 'Антигерой', value: '43' }, + { label: 'Антикварный магазин', value: '44' }, + { label: 'Антимагия', value: '41' }, + { label: 'Антиутопия', value: '225' }, + { label: 'Апатичный главный герой', value: '46' }, + { label: 'Апокалипсис', value: '47' }, + { label: 'Аристократия', value: '51' }, + { label: 'Армия', value: '53' }, + { label: 'Артефакты', value: '58' }, + { label: 'Асоциальный главный герой', value: '42' }, + { label: 'Ассасины', value: '61' }, + { label: 'Атмосфера Европы', value: '245' }, + { label: 'Банды', value: '302' }, + { label: 'БДСМ', value: '77' }, + { label: 'Бедный главный герой', value: '541' }, + { label: 'Бездушный главный герой', value: '143' }, + { label: 'Беззаботный герой', value: '115' }, + { label: 'Безответная любовь', value: '213' }, + { label: 'Безответная любовь', value: '740' }, + { label: 'Безработные', value: '377' }, + { label: 'Беременность', value: '551' }, + { label: 'Бескорыстная любовь', value: '733' }, + { label: 'Бессмертные', value: '359' }, + { label: 'Бесстрашный главный герой', value: '274' }, + { label: 'Библиотека', value: '393' }, + { label: 'Биография', value: '86' }, + { label: 'Близнецы', value: '729' }, + { label: 'Богачи', value: '753' }, + { label: 'Боги', value: '318' }, + { label: 'Богини', value: '316' }, + { label: 'Богоподобный протагонист', value: '314' }, + { label: 'Боевая академия', value: '75' }, + { label: 'Боевая парочка', value: '547' }, + { label: 'Божественная защита', value: '208' }, + { label: 'Божественная сила', value: '317' }, + { label: 'Болеющие персонажи', value: '648' }, + { label: 'Борьба за власть', value: '548' }, + { label: 'Брак по расчету', value: '55' }, + { label: 'Братство', value: '106' }, + { label: 'Братья или сёстры', value: '646' }, + { label: 'Брошенные дети', value: '1' }, + { label: 'Бывший герой', value: '290' }, + { label: 'Вампиры', value: '742' }, + { label: 'Ваншот', value: '496' }, + { label: 'Вдали от родителей', value: '3' }, + { label: 'Ведьмы', value: '756' }, + { label: 'Везучий главный герой', value: '411' }, + { label: 'Верные подданные', value: '410' }, + { label: 'Вероломные герои/плетение интриг', value: '424' }, + { label: 'Взросление персонажа', value: '24' }, + { label: 'Видит то, чего остальной люд не в силах', value: '617' }, + { label: 'Виртуальная реальность', value: '744' }, + { label: 'Владелец магазина', value: '677' }, + { label: 'Владелец уникального оружия', value: '736' }, + { label: 'Властные персонажи', value: '545' }, + { label: 'Влияние прошлого', value: '517' }, + { label: 'Внезапное обогащение', value: '691' }, + { label: 'Внезапное обретение силы', value: '690' }, + { label: 'Военные хроники', value: '749' }, + { label: 'Возвращение в родной мир', value: '587' }, + { label: 'Война', value: '750' }, + { label: 'Вокалоид', value: '745' }, + { label: 'Волшебные твари', value: '413' }, + { label: 'Воскрешение', value: '586' }, + { label: 'Воспитанный главный герой', value: '537' }, + { label: 'Воспоминания прошлого', value: '283' }, + { label: 'Враги становятся союзниками', value: '238' }, + { label: 'Врата в иной мир', value: '303' }, + { label: 'Временной парадокс', value: '711' }, + { label: 'Вторжение на Землю', value: '228' }, + { label: 'Второй шанс', value: '608' }, + { label: 'Выживание', value: '695' }, + { label: 'Вынужденные условия проживания', value: '287' }, + { label: 'Высокомерный герой', value: '56' }, + { label: 'Гадание', value: '207' }, + { label: 'Галлюцинации', value: '189' }, + { label: 'Гарем рабов', value: '656' }, + { label: 'Геймеры', value: '301' }, + { label: 'Генетические модификации', value: '306' }, + { label: 'Герои', value: '341' }, + { label: 'Герои непонятного пола', value: '36' }, + { label: 'Героиня девушка-сорванец', value: '715' }, + { label: 'Героиня красавица', value: '81' }, + { label: 'Герой влюбляется первым', value: '561' }, + { label: 'Герой использует щит', value: '636' }, + { label: 'Герой красавец', value: '329' }, + { label: 'Герой со множеством тел', value: '564' }, + { label: 'Герой-бесстыдник', value: '632' }, + { label: 'Герой-извращенец', value: '521' }, + { label: 'Герой-трудяга', value: '330' }, + { label: 'Герой-яндере', value: '421' }, + { label: 'Гетерохромия', value: '342' }, + { label: 'Гильдии', value: '324' }, + { label: 'Гиперопека', value: '509' }, + { label: 'Главная героиня — женщина', value: '277' }, + { label: 'Главный герой - гений', value: '308' }, + { label: 'Главный герой - Моб', value: '446' }, + { label: 'Главный герой — знаменитость', value: '263' }, + { label: 'Главный герой — мужчина', value: '419' }, + { label: 'Главный герой в поисках гарема', value: '331' }, + { label: 'Главный герой заранее готовится', value: '557' }, + { label: 'Главный герой не человек', value: '488' }, + { label: 'Главный герой приёмный', value: '20' }, + { label: 'Главный герой сразу силён', value: '563' }, + { label: 'Гладиаторы', value: '310' }, + { label: 'Глухой к любви герой', value: '194' }, + { label: 'Гоблины', value: '313' }, + { label: 'Големы', value: '319' }, + { label: 'Гомункул', value: '347' }, + { label: 'Горничные', value: '418' }, + { label: 'Государственные интриги', value: '212' }, + { label: 'Гринд', value: '322' }, + { label: 'Давняя травма', value: '518' }, + { label: 'Двойник', value: '97' }, + { label: 'Дворецкие', value: '111' }, + { label: 'Дворфы', value: '224' }, + { label: 'Дворяне', value: '487' }, + { label: 'Двоюродные братья или сёстры', value: '162' }, + { label: 'Девочки-волшебницы', value: '415' }, + { label: 'Демоны', value: '193' }, + { label: 'Депрессия', value: '196' }, + { label: 'Детектив', value: '198' }, + { label: 'Детективное расследование', value: '473' }, + { label: 'Детское насилие', value: '125' }, + { label: 'Детское обещание', value: '130' }, + { label: 'Дискриминация', value: '203' }, + { label: 'Длительный разрыв отношений', value: '400' }, + { label: 'Домашняя жизнь', value: '45' }, + { label: 'Драконы', value: '218' }, + { label: 'Древний Китай', value: '34' }, + { label: 'Дружба', value: '293' }, + { label: 'Друзья детства', value: '128' }, + { label: 'Друзья становятся врагами', value: '292' }, + { label: 'Духи', value: '673' }, + { label: 'Духовная сила', value: '665' }, + { label: 'Духовный наставник', value: '671' }, + { label: 'Душещипательная история', value: '335' }, + { label: 'Души', value: '666' }, + { label: 'Дьявольский путь культивации', value: '192' }, + { label: 'Ёкай', value: '763' }, + { label: 'Есть аниме-адаптация', value: '10' }, + { label: 'Есть видеоигра', value: '13' }, + { label: 'Есть дорама-адаптация', value: '11' }, + { label: 'Есть манга-адаптация', value: '14' }, + { label: 'Есть манхва-адаптация', value: '16' }, + { label: 'Есть маньхуа-адаптация', value: '15' }, + { label: 'Есть фильм', value: '17' }, + { label: 'Есть CD дорама-адаптация', value: '12' }, + { label: 'Жёсткий главный герой', value: '597' }, + { label: 'Жестокие персонажи', value: '4' }, + { label: 'Жёстокие персонажи', value: '169' }, + { label: 'Животное-спутник', value: '78' }, + { label: 'Жизнь в одиночку', value: '396' }, + { label: 'Жрецы', value: '555' }, + { label: 'Жрицы', value: '554' }, + { label: 'Забота о детях', value: '127' }, + { label: 'Заботливый главный герой', value: '116' }, + { label: 'Загадочное заболевание', value: '471' }, + { label: 'Загадочное прошлое родителей', value: '470' }, + { label: 'Заговоры', value: '154' }, + { label: 'Закалка тела', value: '96' }, + { label: 'Закон джунглей', value: '682' }, + { label: 'Закулисная борба', value: '603' }, + { label: 'Замкнутый главный герой', value: '371' }, + { label: 'Запечатанная сила', value: '607' }, + { label: 'Затворник', value: '346' }, + { label: 'Звери', value: '80' }, + { label: 'Зверолюди', value: '39' }, + { label: 'Звероподобные', value: '79' }, + { label: 'Земледелие', value: '268' }, + { label: 'Злобные благородные девы', value: '743' }, + { label: 'Зловещие организации', value: '247' }, + { label: 'Злой главный герой', value: '248' }, + { label: 'Злые боги', value: '246' }, + { label: 'Злые религии', value: '249' }, + { label: 'Знаменитости', value: '118' }, + { label: 'Знания из прошлой жизни', value: '553' }, + { label: 'Зомби', value: '767' }, + { label: 'Игра на выживание', value: '696' }, + { label: 'Игровая рейтинговая система', value: '300' }, + { label: 'Из грязи в князи', value: '542' }, + { label: 'Известные родители', value: '262' }, + { label: 'Изменение внешнего вида', value: '48' }, + { label: 'Изнасилование', value: '574' }, + { label: 'Инвалидность', value: '202' }, + { label: 'Индустриализация', value: '364' }, + { label: 'Инженер', value: '241' }, + { label: 'Инцест', value: '361' }, + { label: 'Искажённая личность', value: '730' }, + { label: 'Искусственный интеллект', value: '59' }, + { label: 'Исполнение желаний', value: '755' }, + { label: 'Каннибализм', value: '113' }, + { label: 'Карточные игры', value: '114' }, + { label: 'Кладбищенский смотритель', value: '321' }, + { label: 'Классика', value: '134' }, + { label: 'Книги', value: '99' }, + { label: 'Книги навыков', value: '654' }, + { label: 'Книжный червь', value: '100' }, + { label: 'Коллеги', value: '140' }, + { label: 'Колледж/Университет', value: '145' }, + { label: 'Командная работа', value: '701' }, + { label: 'Комедийный подтекст', value: '147' }, + { label: 'Комплекс брата', value: '105' }, + { label: 'Комплекс неполноценности', value: '365' }, + { label: 'Контракты', value: '155' }, + { label: 'Контроль разума', value: '441' }, + { label: 'Конфликт в семье', value: '261' }, + { label: 'Конфликт верности', value: '153' }, + { label: 'Копейщик', value: '668' }, + { label: 'Королевская особа', value: '596' }, + { label: 'Королевства', value: '382' }, + { label: 'Короткий рассказ', value: '638' }, + { label: 'Коррупция', value: '157' }, + { label: 'Космические Войны', value: '158' }, + { label: 'Космос', value: '507' }, + { label: 'Косплей', value: '159' }, + { label: 'Кража способностей', value: '2' }, + { label: 'Крафтинг', value: '164' }, + { label: 'Кризис личности', value: '357' }, + { label: 'Кровь', value: '320' }, + { label: 'Кружки, клубы', value: '138' }, + { label: 'Кудэрэ', value: '384' }, + { label: 'Кузнец', value: '90' }, + { label: 'Кукловоды', value: '567' }, + { label: 'Куклы/Марионетки', value: '211' }, + { label: 'Кулинария', value: '156' }, + { label: 'Культивация', value: '171' }, + { label: 'Куннилингус', value: '172' }, + { label: 'Легенды', value: '391' }, + { label: 'Легкая жизнь', value: '229' }, + { label: 'Лекари', value: '334' }, + { label: 'Ленивый главный герой', value: '389' }, + { label: 'Лидерство', value: '390' }, + { label: 'Любит персонажа младше себя', value: '765' }, + { label: 'Любит персонажа старше себя', value: '494' }, + { label: 'Любовное соперничество', value: '406' }, + { label: 'Любовные треугольники', value: '407' }, + { label: 'Любовь детства', value: '129' }, + { label: 'Любовь с первого взгляда', value: '404' }, + { label: 'Любящие родители', value: '215' }, + { label: 'Маги', value: '757' }, + { label: 'Магические технологии', value: '417' }, + { label: 'Магия', value: '412' }, + { label: 'Магия призыва', value: '694' }, + { label: 'Маленькие девочки', value: '397' }, + { label: 'Мангака', value: '423' }, + { label: 'Маниакальная любовь', value: '492' }, + { label: 'Марти Сью', value: '508' }, + { label: 'Мастер подземелий', value: '222' }, + { label: 'Мастурбация', value: '432' }, + { label: 'Матриархат', value: '433' }, + { label: 'Межпространственные путешествия', value: '370' }, + { label: 'Менеджмент', value: '422' }, + { label: 'Месть', value: '588' }, + { label: 'Мечи и магия', value: '697' }, + { label: 'Мечники', value: '698' }, + { label: 'Миленькие дети', value: '176' }, + { label: 'Милитаризм', value: '439' }, + { label: 'Миловидная история', value: '178' }, + { label: 'Милый главный герой', value: '177' }, + { label: 'Минет', value: '275' }, + { label: 'Мировое древо', value: '760' }, + { label: 'Миролюбивый главный герой', value: '510' }, + { label: 'Мифология', value: '475' }, + { label: 'Младшие сёстры', value: '766' }, + { label: 'ММОРПГ', value: '445' }, + { label: 'Множество временных линий', value: '463' }, + { label: 'Модели', value: '447' }, + { label: 'Молод не по годам', value: '49' }, + { label: 'Монстры', value: '454' }, + { label: 'Мрачность', value: '183' }, + { label: 'Музыка', value: '466' }, + { label: 'Музыкальные группы', value: '68' }, + { label: 'Мутации', value: '468' }, + { label: 'На волосок от смерти', value: '479' }, + { label: 'На все руки мастер', value: '374' }, + { label: 'Навыки имеют стоимость', value: '150' }, + { label: 'Нагота', value: '490' }, + { label: 'Наездники на драконах', value: '216' }, + { label: 'Наёмники', value: '437' }, + { label: 'Наивный главный герой', value: '476' }, + { label: 'Наследие', value: '366' }, + { label: 'Настоящая любовь', value: '200' }, + { label: 'Настоящий гарем', value: '540' }, + { label: 'Небесное испытание', value: '337' }, + { label: 'Невезучий главный герой', value: '738' }, + { label: 'Недоверчивый протагонист', value: '206' }, + { label: 'Недооцененный главный герой', value: '734' }, + { label: 'Недопонимание', value: '444' }, + { label: 'Некромант', value: '480' }, + { label: 'Нелинейное повествование', value: '489' }, + { label: 'Неловкий главный герой', value: '67' }, + { label: 'Нелюбимый протагонист', value: '333' }, + { label: 'Немой персонаж', value: '469' }, + { label: 'Ненадежный рассказчик', value: '739' }, + { label: 'Непримечательный герой', value: '65' }, + { label: 'Нерешительный протагонист', value: '363' }, + { label: 'Несгибаемый главный герой', value: '676' }, + { label: 'Несколько главных героев', value: '460' }, + { label: 'Несколько миров', value: '461' }, + { label: 'Несколько перерожденцев', value: '462' }, + { label: 'Несколько попаданцев', value: '464' }, + { label: 'Несравненная скорость культивации', value: '269' }, + { label: 'Нехватка/отсутствие здравого смысла', value: '385' }, + { label: 'Нечестный протагонист', value: '205' }, + { label: 'НИИТ', value: '481' }, + { label: 'Ниндзя', value: '486' }, + { label: 'Обмен телами', value: '95' }, + { label: 'Оборотни', value: '754' }, + { label: 'Объект любви влюбился первым', value: '405' }, + { label: 'Объект любви популярен', value: '543' }, + { label: 'Огнестрельное оружие', value: '280' }, + { label: 'Ограничение на время жизни', value: '394' }, + { label: 'Одержимость', value: '544' }, + { label: 'Один родитель', value: '651' }, + { label: 'Одинокий главный герой', value: '399' }, + { label: 'Одиночество', value: '398' }, + { label: 'Одно тело на двоих', value: '634' }, + { label: 'Око силы', value: '253' }, + { label: 'Оммёдзи', value: '498' }, + { label: 'Организованная преступность', value: '500' }, + { label: 'Оргия', value: '501' }, + { label: 'Орки', value: '499' }, + { label: 'Основано на аниме', value: '74' }, + { label: 'Основано на видеоигре', value: '72' }, + { label: 'Основано на визуальной новелле', value: '73' }, + { label: 'Основано на песне', value: '70' }, + { label: 'Основано на фильме', value: '69' }, + { label: 'Осторожный главный герой', value: '117' }, + { label: 'От ненависти до любви один шаг', value: '239' }, + { label: 'От сильного с сильнейшему', value: '684' }, + { label: 'От слабого к сильному', value: '752' }, + { label: 'Отаку', value: '503' }, + { label: 'Отзывчивая любовь', value: '380' }, + { label: 'Отзывчивый главный герой', value: '339' }, + { label: 'Отношение учитель-ученик', value: '430' }, + { label: 'Отношения бог-человек', value: '315' }, + { label: 'Отношения между учителем и учеником', value: '687' }, + { label: 'Отношения начальник-подчинённый', value: '101' }, + { label: 'Отношения против воли', value: '286' }, + { label: 'Отношения с нечеловеком', value: '353' }, + { label: 'Отношения сэмпай-кохай', value: '621' }, + { label: 'Отношения хозяин-слуга', value: '431' }, + { label: 'Отомэ-игры', value: '504' }, + { label: 'ОТП', value: '562' }, + { label: 'Отшельники', value: '505' }, + { label: 'Офисный работник', value: '600' }, + { label: 'Официанты', value: '748' }, + { label: 'Охотники', value: '355' }, + { label: 'Падшие ангелы', value: '255' }, + { label: 'Падшие дворяне', value: '256' }, + { label: 'Пайзури', value: '511' }, + { label: 'Паразиты', value: '513' }, + { label: 'Параллельные миры', value: '512' }, + { label: 'Парк развлечений', value: '32' }, + { label: 'Пародия', value: '515' }, + { label: 'Перемещение между мирами', value: '758' }, + { label: 'Переодевание', value: '167' }, + { label: 'Переплавка пилюль', value: '529' }, + { label: 'Перерождение в девушку', value: '420' }, + { label: 'Перерождение в другом мире', value: '580' }, + { label: 'Перерождение в другом мире', value: '720' }, + { label: 'Перерождение в игровом мире', value: '579' }, + { label: 'Перерождение в монстра', value: '577' }, + { label: 'Персонажи с мазохистскими наклонностями', value: '429' }, + { label: 'Персонажи с садистскими наклонностями', value: '598' }, + { label: 'Петля времени', value: '709' }, + { label: 'Петтинг', value: '328' }, + { label: 'Писатели', value: '761' }, + { label: 'Питомцы', value: '522' }, + { label: 'Племенное общество', value: '726' }, + { label: 'Повелитель демонов', value: '191' }, + { label: 'Повествование от нескольких лиц', value: '459' }, + { label: 'Поглощение навыков', value: '653' }, + { label: 'Подземелья', value: '223' }, + { label: 'Поздно начинающаяся романтическая линия', value: '387' }, + { label: 'Политика', value: '538' }, + { label: 'Полулюди', value: '190' }, + { label: 'Помолвка', value: '240' }, + { label: 'Попаданец в другой мир', value: '723' }, + { label: 'Попаданец в игровой мир', value: '722' }, + { label: 'Постапокалипсис', value: '546' }, + { label: 'Постижение Дао', value: '181' }, + { label: 'Потеря девственности', value: '282' }, + { label: 'Потустороннее пространство', value: '416' }, + { label: 'Похищения', value: '379' }, + { label: 'Прагматичный главный герой', value: '549' }, + { label: 'Предательство', value: '83' }, + { label: 'Предвидение', value: '550' }, + { label: 'Преследователи', value: '674' }, + { label: 'Преступление', value: '165' }, + { label: 'Преступность', value: '166' }, + { label: 'Приёмные дети', value: '19' }, + { label: 'Призванный герой', value: '693' }, + { label: 'Призраки', value: '309' }, + { label: 'Приключения', value: '22' }, + { label: 'Принуждение', value: '108' }, + { label: 'Приручатель', value: '453' }, + { label: 'Прислуга', value: '625' }, + { label: 'Приставучий любовник', value: '136' }, + { label: 'Притворная любовь', value: '552' }, + { label: 'Пришельцы', value: '28' }, + { label: 'Программист', value: '558' }, + { label: 'Произведение - обладатель наград', value: '66' }, + { label: 'Проклятия', value: '175' }, + { label: 'Промывка мозгов', value: '102' }, + { label: 'Пропуск времени', value: '712' }, + { label: 'Пророчества', value: '559' }, + { label: 'Протагонист в очках', value: '312' }, + { label: 'Протагонист раб', value: '657' }, + { label: 'Психокинез', value: '565' }, + { label: 'Психопаты', value: '566' }, + { label: 'Путешествия', value: '759' }, + { label: 'Путешествия во времени', value: '713' }, + { label: 'Пытки', value: '716' }, + { label: 'Рабы', value: '658' }, + { label: 'Развитие отношений', value: '160' }, + { label: 'Развитие персонажа', value: '119' }, + { label: 'Раздвоение личности', value: '458' }, + { label: 'Размеренная романтика', value: '661' }, + { label: 'Разница в статусе', value: '201' }, + { label: 'Разорванная помолвка', value: '104' }, + { label: 'Разумные предметы', value: '622' }, + { label: 'Рай', value: '336' }, + { label: 'Раняя романтика', value: '227' }, + { label: 'Расизм', value: '573' }, + { label: 'Расследования', value: '372' }, + { label: 'Ребенок — главный герой', value: '126' }, + { label: 'Реверс-гарем', value: '589' }, + { label: 'Реверс-изнасилование', value: '590' }, + { label: 'Резкая смена характера', value: '520' }, + { label: 'Реинкарнация', value: '581' }, + { label: 'Религии', value: '582' }, + { label: 'Ресторан', value: '585' }, + { label: 'Решительный главный герой', value: '199' }, + { label: 'Робкий протагонист', value: '714' }, + { label: 'Родительсткий комплекс', value: '514' }, + { label: 'Родные не связанные кровью', value: '647' }, + { label: 'Родословные', value: '94' }, + { label: 'РПГ-система', value: '392' }, + { label: 'Рыцари', value: '383' }, + { label: 'Самовлюбленный главный герой', value: '477' }, + { label: 'Самоубийства', value: '692' }, + { label: 'Самураи', value: '601' }, + { label: 'Сборник коротких историй', value: '144' }, + { label: 'Свадьба', value: '426' }, + { label: 'Связанные сюжетные линии', value: '369' }, + { label: 'Святые', value: '599' }, + { label: 'Секреты', value: '614' }, + { label: 'Секс втроём', value: '707' }, + { label: 'Секс-рабыня', value: '629' }, + { label: 'Семейная любовь', value: '257' }, + { label: 'Семь добродетелей', value: '627' }, + { label: 'Семь смертных грехов', value: '626' }, + { label: 'Серийные убийцы', value: '624' }, + { label: 'Сикигами', value: '637' }, + { label: 'Сильная любовь от старших', value: '214' }, + { label: 'Синдром восьмиклассника', value: '132' }, + { label: 'Синдром сестры', value: '652' }, + { label: 'Сироты', value: '502' }, + { label: 'Системный администратор', value: '699' }, + { label: 'Скромный главный герой', value: '409' }, + { label: 'Скрытые романтические отношения', value: '23' }, + { label: 'Скрытые способности', value: '343' }, + { label: 'Скряга', value: '450' }, + { label: 'Скульпторы', value: '606' }, + { label: 'Слабая романтика', value: '688' }, + { label: 'Слабый главный герой', value: '751' }, + { label: 'Сложные семейные отношения', value: '149' }, + { label: 'Смена расы', value: '572' }, + { label: 'Смертельное заболевание', value: '704' }, + { label: 'Смерть', value: '185' }, + { label: 'Смерть любимых', value: '186' }, + { label: 'Современность', value: '448' }, + { label: 'Современные знания в слаборазвитых мирах', value: '449' }, + { label: 'Сожительство', value: '141' }, + { label: 'Создание артефактов', value: '57' }, + { label: 'Создание клана', value: '133' }, + { label: 'Создание королевства', value: '381' }, + { label: 'Создание навыков', value: '655' }, + { label: 'Сокрытие истинных способностей', value: '344' }, + { label: 'Сокрытие личности', value: '345' }, + { label: 'Солдаты', value: '664' }, + { label: 'Сон', value: '659' }, + { label: 'Соседи по комнате', value: '595' }, + { label: 'Социальные изгои', value: '663' }, + { label: 'Спасение мира', value: '602' }, + { label: 'Спорящая пара', value: '84' }, + { label: 'Способность перевоплощения', value: '719' }, + { label: 'Средневековье', value: '436' }, + { label: 'Стеснительные персонажи', value: '643' }, + { label: 'Стокгольмский синдром', value: '575' }, + { label: 'Стокгольмский синдром', value: '675' }, + { label: 'Стратег', value: '681' }, + { label: 'Стратегические битвы', value: '680' }, + { label: 'Стрелки', value: '325' }, + { label: 'Стрельба из лука', value: '50' }, + { label: 'Студенческий совет', value: '686' }, + { label: 'Судьба', value: '197' }, + { label: 'Суккубы', value: '689' }, + { label: 'Суперспособности', value: '669' }, + { label: 'Суровая тренировка', value: '332' }, + { label: 'Сцены насилия', value: '195' }, + { label: 'Таинственное прошлое', value: '472' }, + { label: 'Тайная личность', value: '610' }, + { label: 'Тайные организации', value: '611' }, + { label: 'Телохранители', value: '98' }, + { label: 'Тентакли', value: '703' }, + { label: 'Террористы', value: '705' }, + { label: 'Технологический разрыв', value: '702' }, + { label: 'Тихие герои', value: '568' }, + { label: 'Толстый протагонист', value: '271' }, + { label: 'Торговцы', value: '438' }, + { label: 'Травник', value: '340' }, + { label: 'Трагичное прошлое', value: '718' }, + { label: 'Трап', value: '725' }, + { label: 'Триллер', value: '708' }, + { label: 'Турнир', value: '76' }, + { label: 'Тюрьма', value: '556' }, + { label: 'Убийства', value: '465' }, + { label: 'Уверенный в себе главный герой', value: '151' }, + { label: 'Умная пара', value: '662' }, + { label: 'Умный главный герой', value: '135' }, + { label: 'Уникальная техника культивации', value: '735' }, + { label: 'Уникальные оружия', value: '737' }, + { label: 'Управление бизнесом', value: '109' }, + { label: 'Управление временем', value: '710' }, + { label: 'Управление пространством', value: '667' }, + { label: 'Ускоренный рост', value: '6' }, + { label: 'Учёные', value: '605' }, + { label: 'Учителя', value: '700' }, + { label: 'Фамильяры', value: '258' }, + { label: 'Фанатизм', value: '264' }, + { label: 'Фармацевт', value: '523' }, + { label: 'Феи', value: '254' }, + { label: 'Фениксы', value: '526' }, + { label: 'Фетиш на грудь', value: '103' }, + { label: 'Фетиш на очки', value: '311' }, + { label: 'Философия', value: '524' }, + { label: 'Фобии/Страхи', value: '525' }, + { label: 'Фольклор', value: '285' }, + { label: 'Формирование армии', value: '54' }, + { label: 'Футанари', value: '295' }, + { label: 'Футуристический мир', value: '296' }, + { label: 'Фэнтези', value: '267' }, + { label: 'Фэнтезийные существа', value: '266' }, + { label: 'Хакеры', value: '326' }, + { label: 'Харизматичный протагонист', value: '120' }, + { label: 'Хитрый главный герой', value: '173' }, + { label: 'Хладнокровный главный герой', value: '112' }, + { label: 'Художники', value: '60' }, + { label: 'Хулиганы', value: '188' }, + { label: 'Цундере', value: '728' }, + { label: 'Чат', value: '122' }, + { label: 'Человек-оружие', value: '352' }, + { label: 'Человекоподобный главный герой', value: '354' }, + { label: 'Честный главный герой', value: '348' }, + { label: 'Четкие любовные убеждения', value: '683' }, + { label: 'Читы', value: '123' }, + { label: 'Чудаковатые герои', value: '569' }, + { label: 'Чужие воспоминания', value: '721' }, + { label: 'Шантаж', value: '89' }, + { label: 'Шеф-повар', value: '124' }, + { label: 'Школа для девочек', value: '29' }, + { label: 'Шоу-бизнес', value: '642' }, + { label: 'Шпионы', value: '670' }, + { label: 'Эволюция', value: '250' }, + { label: 'Эгоистичный главный герой', value: '618' }, + { label: 'Экзорцизм', value: '252' }, + { label: 'Экономика', value: '230' }, + { label: 'Эксгибиционизм', value: '251' }, + { label: 'Эксперименты на людях', value: '351' }, + { label: 'Элементальная магия', value: '234' }, + { label: 'Элементы игрового мира', value: '299' }, + { label: 'Элементы романтики', value: '594' }, + { label: 'Элементы юри', value: '640' }, + { label: 'Элементы яоя', value: '641' }, + { label: 'Эльфы', value: '235' }, + { label: 'Эпизодичность', value: '243' }, + { label: 'Язвительные персонажи', value: '635' }, + { label: 'Языковой барьер', value: '386' }, + { label: 'Яндере', value: '762' }, + { label: 'Яойщица', value: '294' }, + { label: 'Японские силы самообороны', value: '378' }, + { label: 'R-15', value: '570' }, + { label: 'R-18', value: '571' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + label: 'Жанры', + value: [], + options: [ + { label: '16+', value: '1' }, + { label: '18+', value: '2' }, + { label: 'Боевик', value: '3' }, + { label: 'Боевые искусства', value: '4' }, + { label: 'Вампиры', value: '47' }, + { label: 'Военное', value: '44' }, + { label: 'Гарем', value: '5' }, + { label: 'Демоны', value: '48' }, + { label: 'Детектив', value: '6' }, + { label: 'Дзёсей', value: '50' }, + { label: 'Драма', value: '7' }, + { label: 'Игры', value: '45' }, + { label: 'Историческое', value: '8' }, + { label: 'Киберпанк', value: '9' }, + { label: 'Комедия', value: '10' }, + { label: 'Космос', value: '46' }, + { label: 'Магия', value: '42' }, + { label: 'Махо-сёдзё', value: '12' }, + { label: 'Меха', value: '13' }, + { label: 'Мистика', value: '14' }, + { label: 'Музыка', value: '49' }, + { label: 'Научная фантастика', value: '15' }, + { label: 'Пародия', value: '16' }, + { label: 'Повседневность', value: '17' }, + { label: 'Приключения', value: '18' }, + { label: 'Психологическое', value: '19' }, + { label: 'Романтика', value: '20' }, + { label: 'Сверхъестественное', value: '21' }, + { label: 'Сёдзё', value: '22' }, + { label: 'Сёдзё-ай', value: '23' }, + { label: 'Сёнэн', value: '24' }, + { label: 'Сёнэн-ай', value: '25' }, + { label: 'Смена пола', value: '26' }, + { label: 'Современность', value: '27' }, + { label: 'Спорт', value: '28' }, + { label: 'Супер сила', value: '43' }, + { label: 'Сэйнэн', value: '29' }, + { label: 'Трагедия', value: '32' }, + { label: 'Триллер', value: '33' }, + { label: 'Ужасы', value: '34' }, + { label: 'Уся', value: '35' }, + { label: 'Фэнтези', value: '36' }, + { label: 'Школьная жизнь', value: '37' }, + { label: 'Этти', value: '38' }, + { label: 'Юри', value: '39' }, + { label: 'Яой', value: '40' }, + { label: 'LitRPG', value: '41' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new TL(); + +type response = { + data: Data; +}; + +type Data = { + projects?: Projects; + project?: Project; + chapter?: DataChapter; +}; + +type DataChapter = { + text: Text; +}; + +type Text = { + text: string; +}; + +type Project = { + title: string; + translationStatus: string; + fullUrl: string; + covers: Cover[]; + persons: Person[]; + genres: Genre[]; + tags: Genre[]; + annotation: Text; + subprojects: Subprojects; +}; + +type Cover = { + url: string; +}; + +type Genre = { + nameRu: string; + nameEng: string; +}; + +type Person = { + role: string; + name: Name; +}; + +type Name = { + firstName: string; + lastName: string; +}; + +type Subprojects = { + content: SubprojectsContent[]; +}; + +type SubprojectsContent = { + title: null | string; + volumes: Volumes; +}; + +type Volumes = { + content: VolumesContent[]; +}; + +type VolumesContent = { + shortName: null | string; + chapters: ChapterElement[]; +}; + +type ChapterElement = { + title: null | string; + publishDate: Date; + fullUrl: string; + published: boolean; +}; + +type Projects = { + content: ProjectsContent[]; +}; + +type ProjectsContent = { + title: string; + fullUrl: string; + covers: Cover[]; +}; diff --git a/plugins/russian/ranobehub.ts b/plugins/russian/ranobehub.ts new file mode 100644 index 000000000..0459db036 --- /dev/null +++ b/plugins/russian/ranobehub.ts @@ -0,0 +1,1279 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +const statusKey: Record<number, string> = { + 1: NovelStatus.Ongoing, + 2: NovelStatus.Completed, + 3: NovelStatus.OnHiatus, +}; + +class RNBH implements Plugin.PluginBase { + id = 'RNBH.org'; + name = 'RanobeHub'; + version = '1.0.2'; + site = 'https://ranobehub.org'; + icon = 'src/ru/ranobehub/icon.png'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/api/search?page=' + pageNo + '&sort='; + url += showLatestNovels + ? 'last_chapter_at' + : filters?.sort?.value || 'computed_rating'; + url += '&status=' + (filters?.status?.value ? filters?.status?.value : '0'); + + if (filters) { + if (filters.country?.value?.length) { + url += '&country=' + filters.country.value.join(','); + } + + const includeTags = [ + filters.tags?.value?.include, + filters.events?.value?.include, + ] + .flat() + .filter(t => t); + + if (includeTags.length) { + url += '&tags:positive=' + includeTags.join(','); + } + + const excludeTags = [ + filters.tags?.value?.exclude, + filters.events?.value?.exclude, + ] + .flat() + .filter(t => t); + + if (excludeTags.length) { + url += '&tags:negative=' + excludeTags.join(','); + } + } + const { resource }: { resource: responseNovels[] } = await fetchApi( + url + '&take=40', + ).then(res => res.json()); + + const novels: Plugin.NovelItem[] = []; + resource.forEach(novel => + novels.push({ + name: novel.names.rus || novel.names.eng || novel.names.original, + cover: novel.poster.medium, + path: novel.id.toString(), + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const { data }: { data: responseNovel } = await fetchApi( + this.site + '/api/ranobe/' + novelPath, + ).then(res => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: data.names.rus || data.names.eng || '', + cover: data.posters.medium, + summary: data.description.trim(), + author: data?.authors?.[0]?.name_eng || '', + status: statusKey[data.status.id] || NovelStatus.Unknown, + }; + + const tags = [data.tags.events, data.tags.genres] + .flat() + .map(tags => tags?.names?.rus || tags?.names?.eng || tags?.title) + .filter(tags => tags); + + if (tags.length) { + novel.genres = tags.join(', '); + } + + const chapters: Plugin.ChapterItem[] = []; + const chaptersJSON: { volumes: VolumesEntity[] } = await fetchApi( + this.site + '/api/ranobe/' + novelPath + '/contents', + ).then(res => res.json()); + + chaptersJSON.volumes.forEach(volume => + volume.chapters?.forEach(chapter => + chapters.push({ + name: chapter.name, + path: novelPath + '/' + volume.num + '/' + chapter.num, + releaseTime: dayjs(parseInt(chapter.changed_at, 10) * 1000).format( + 'LLL', + ), + chapterNumber: chapters.length + 1, + }), + ), + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.resolveUrl(chapterPath)).then(res => + res.text(), + ); + + const indexA = body.indexOf('<div class="title-wrapper">'); + const indexB = body.indexOf('<div class="ui text container"', indexA); + + const chapterText = body + .substring(indexA, indexB) + .replace(/<img data-media-id="(.*?)".*?>/g, '<img src="/api/media/$1">'); + + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/api/fulltext/global?query=${searchTerm}&take=10`; + const result: responseSearch[] = await fetchApi(url).then(res => + res.json(), + ); + const novels: Plugin.NovelItem[] = []; + + result + ?.find(item => item?.meta?.key === 'ranobe') + ?.data?.forEach(novel => + novels.push({ + name: + novel?.names?.rus || + novel?.names?.eng || + novel.name || + novel?.names?.original || + '', + path: novel.id.toString(), + cover: novel?.image?.replace('/small', '/medium'), + }), + ); + + return novels; + } + + resolveUrl = (path: string) => this.site + '/ranobe/' + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'computed_rating', + options: [ + { label: 'по рейтингу', value: 'computed_rating' }, + { label: 'по дате обновления', value: 'last_chapter_at' }, + { label: 'по дате добавления', value: 'created_at' }, + { label: 'по названию', value: 'name_rus' }, + { label: 'по просмотрам', value: 'views' }, + { label: 'по количеству глав', value: 'count_chapters' }, + { label: 'по объему перевода', value: 'count_of_symbols' }, + ], + type: FilterTypes.Picker, + }, + status: { + label: 'Статус перевода', + value: '', + options: [ + { label: 'Любой', value: '' }, + { label: 'В процессе', value: '1' }, + { label: 'Завершено', value: '2' }, + { label: 'Заморожено', value: '3' }, + { label: 'Неизвестно', value: '4' }, + ], + type: FilterTypes.Picker, + }, + country: { + label: 'Тип', + value: [], + options: [ + { label: 'Китай', value: '2' }, + { label: 'Корея', value: '3' }, + { label: 'США', value: '4' }, + { label: 'Япония', value: '1' }, + ], + type: FilterTypes.CheckboxGroup, + }, + events: { + label: 'События', + value: { include: [], exclude: [] }, + options: [ + { label: '[Награжденная работа]', value: '611' }, + { label: '18+', value: '338' }, + { label: 'Авантюристы', value: '353' }, + { label: 'Автоматоны', value: '538' }, + { label: 'Агрессивные персонажи', value: '434' }, + { label: 'Ад', value: '509' }, + { label: 'Адаптация в радиопостановку', value: '522' }, + { label: 'Академия', value: '25' }, + { label: 'Актеры озвучки', value: '578' }, + { label: 'Активный главный герой', value: '132' }, + { label: 'Алхимия', value: '116' }, + { label: 'Альтернативный мир', value: '28' }, + { label: 'Амнезия/Потеря памяти', value: '247' }, + { label: 'Анабиоз', value: '657' }, + { label: 'Ангелы', value: '218' }, + { label: 'Андрогинные персонажи', value: '217' }, + { label: 'Андроиды', value: '82' }, + { label: 'Анти-магия', value: '471' }, + { label: 'Антигерой', value: '346' }, + { label: 'Антикварный магазин', value: '572' }, + { label: 'Антисоциальный главный герой', value: '562' }, + { label: 'Антиутопия', value: '663' }, + { label: 'Апатичный протагонист', value: '29' }, + { label: 'Апокалипсис', value: '314' }, + { label: 'Аранжированный брак', value: '285' }, + { label: 'Армия', value: '598' }, + { label: 'Артефакты', value: '117' }, + { label: 'Артисты', value: '460' }, + { label: 'Банды', value: '581' }, + { label: 'БДСМ', value: '676' }, + { label: 'Бедный главный герой', value: '309' }, + { label: 'Безжалостный главный герой', value: '144' }, + { label: 'Беззаботный главный герой', value: '355' }, + { label: 'Безусловная любовь', value: '650' }, + { label: 'Беременность', value: '131' }, + { label: 'Бесполый главный герой', value: '222' }, + { label: 'Бессмертные', value: '275' }, + { label: 'Бесстрашный протагонист', value: '619' }, + { label: 'Бесстыдный главный герой', value: '256' }, + { label: 'Бесчестный главный герой', value: '699' }, + { label: 'Библиотека', value: '342' }, + { label: 'Бизнесмен ', value: '813' }, + { label: 'Биочип', value: '120' }, + { label: 'Бисексуальный главный герой', value: '822' }, + { label: 'Близнецы', value: '148' }, + { label: 'Боги', value: '211' }, + { label: 'Богини', value: '356' }, + { label: 'Боевая академия', value: '369' }, + { label: 'Боевые духи', value: '347' }, + { label: 'Боевые соревнования', value: '422' }, + { label: 'Божественная защита', value: '336' }, + { label: 'Божественные силы', value: '224' }, + { + label: + 'Большая разница в возрасте между героем и его любовным интересом', + value: '348', + }, + { label: 'Борьба за власть', value: '544' }, + { label: 'Брак', value: '363' }, + { label: 'Брак по расчету', value: '65' }, + { label: 'Братский комплекс', value: '31' }, + { label: 'Братство', value: '413' }, + { label: 'Братья и сестры', value: '518' }, + { label: 'Буддизм', value: '742' }, + { label: 'Быстрая культивация', value: '273' }, + { label: 'Быстрообучаемый', value: '221' }, + { label: 'Валькирии', value: '667' }, + { label: 'Вампиры', value: '266' }, + { label: 'Ваншот', value: '679' }, + { label: 'Ведьмы', value: '169' }, + { label: 'Вежливый главный герой', value: '289' }, + { label: 'Верные подчиненные', value: '225' }, + { label: 'Взрослый главный герой', value: '183' }, + { label: 'Видит то, чего не видят другие', value: '636' }, + { label: 'Виртуальная реальность', value: '313' }, + { label: 'Владелец магазина', value: '653' }, + { label: 'Внезапная сила', value: '376' }, + { label: 'Внезапное богатство', value: '802' }, + { + label: 'Внешний вид отличается от фактического возраста', + value: '334', + }, + { label: 'Военные Летописи', value: '740' }, + { label: 'Возвращение из другого мира', value: '673' }, + { label: 'Войны', value: '58' }, + { label: 'Вокалоид', value: '678' }, + { label: 'Волшебники/Волшебницы', value: '477' }, + { label: 'Волшебные звери', value: '201' }, + { label: 'Воображаемый друг', value: '614' }, + { label: 'Воры', value: '326' }, + { label: 'Воскрешение', value: '78' }, + { label: 'Враги становятся возлюбленными', value: '428' }, + { label: 'Враги становятся союзниками', value: '502' }, + { label: 'Врата в другой мир', value: '558' }, + { label: 'Врачи', value: '286' }, + { label: 'Временной парадокс', value: '163' }, + { label: 'Всемогущий главный герой', value: '42' }, + { label: 'Вторжение на землю', value: '77' }, + { label: 'Второй шанс', value: '112' }, + { label: 'Вуайеризм', value: '851' }, + { label: 'Выживание', value: '290' }, + { label: 'Высокомерные персонажи', value: '268' }, + { label: 'Гадание', value: '540' }, + { label: 'Гарем рабов', value: '828' }, + { label: 'Геймеры', value: '302' }, + { label: 'Генералы', value: '223' }, + { label: 'Генетические модификации', value: '620' }, + { label: 'Гениальный главный герой', value: '566' }, + { label: 'Герои', value: '173' }, + { label: 'Героиня — сорванец', value: '525' }, + { label: 'Герой влюбляется первым', value: '64' }, + { label: 'Гетерохромия', value: '510' }, + { label: 'Гильдии', value: '323' }, + { label: 'Гипнотизм', value: '768' }, + { label: 'Главный герой — бог', value: '486' }, + { label: 'Главный герой — гуманоид', value: '595' }, + { label: 'Главный герой — женщина', value: '63' }, + { label: 'Главный герой — мужчина', value: '39' }, + { label: 'Главный герой — наполовину человек', value: '362' }, + { label: 'Главный герой — отец', value: '859' }, + { label: 'Главный герой — раб', value: '832' }, + { label: 'Главный герой — ребенок', value: '415' }, + { label: 'Главный герой — рубака', value: '400' }, + { label: 'Главный герой — собиратель гарема', value: '439' }, + { label: 'Главный герой влюбляется первым', value: '655' }, + { label: 'Главный герой играет роль', value: '396' }, + { label: 'Главный герой носит очки', value: '637' }, + { label: 'Главный герой пацифист', value: '675' }, + { label: 'Главный герой с несколькими телами', value: '628' }, + { label: 'Главный герой силен с самого начала', value: '45' }, + { label: 'Гладиаторы', value: '549' }, + { label: 'Глуповатый главный герой', value: '295' }, + { label: 'Гоблины', value: '529' }, + { label: 'Големы', value: '569' }, + { label: 'Гомункул', value: '850' }, + { label: 'Горничные', value: '380' }, + { label: 'Госпиталь', value: '1043' }, + { label: 'Готовка', value: '193' }, + { label: 'Гриндинг', value: '303' }, + { label: 'Дао Компаньон', value: '384' }, + { label: 'Даосизм', value: '792' }, + { label: 'Дарк', value: '151' }, + { label: 'Дварфы', value: '220' }, + { label: 'Двойная личность', value: '601' }, + { label: 'Двойник', value: '547' }, + { label: 'Дворецкий', value: '623' }, + { label: 'Дворяне', value: '41' }, + { label: 'Дворянство/Аристократия', value: '354' }, + { label: 'Девушки-монстры', value: '634' }, + { label: 'Демоническая техника культивации', value: '706' }, + { label: 'Демоны', value: '6' }, + { label: 'Денежный долг', value: '841' }, + { label: 'Депрессия', value: '494' }, + { label: 'Детективы', value: '561' }, + { label: 'Дискриминация', value: '34' }, + { + label: 'Добыча денег одно из основных стремлений главного героя', + value: '307', + }, + { label: 'Долгая разлука', value: '200' }, + { label: 'Домашние дела', value: '178' }, + { label: 'Домогательство', value: '394' }, + { label: 'Драконы', value: '195' }, + { label: 'Драконьи всадники', value: '555' }, + { label: 'Древние времена', value: '102' }, + { label: 'Древний Китай', value: '284' }, + { label: 'Дружба', value: '97' }, + { label: 'Друзья детства', value: '170' }, + { label: 'Друзья становятся врагами', value: '507' }, + { label: 'Друиды', value: '427' }, + { label: 'Дух лисы', value: '842' }, + { label: 'Духи/Призраки', value: '46' }, + { label: 'Духовный советник', value: '351' }, + { label: 'Душевность', value: '587' }, + { label: 'Души', value: '136' }, + { label: 'Европейская атмосфера', value: '457' }, + { label: 'Ёкаи', value: '516' }, + { label: 'Есть аниме-адаптация', value: '26' }, + { label: 'Есть видеоигра по мотивам', value: '491' }, + { label: 'Есть манга-адаптация', value: '27' }, + { label: 'Есть манхва-адаптация', value: '453' }, + { label: 'Есть маньхуа-адаптация', value: '298' }, + { label: 'Есть сериал-адаптация', value: '421' }, + { label: 'Есть фильм по мотивам', value: '47' }, + { label: 'Женища-наставник', value: '438' }, + { label: 'Жертва изнасилования влюбляется в насильника', value: '414' }, + { label: 'Жесткая, двуличная личность', value: '249' }, + { label: 'Жестокие персонажи', value: '436' }, + { label: 'Жестокое обращение с ребенком', value: '617' }, + { label: 'Жестокость', value: '127' }, + { label: 'Животноводство', value: '765' }, + { label: 'Животные черты', value: '466' }, + { label: 'Жизнь в квартире', value: '554' }, + { label: 'Жрицы', value: '564' }, + { label: 'Заботливый главный герой', value: '176' }, + { label: 'Забывчивый главный герой', value: '579' }, + { label: 'Заговоры', value: '177' }, + { label: 'Закалка тела', value: '269' }, + { label: 'Законники', value: '769' }, + { label: 'Замкнутый главный герой', value: '533' }, + { label: 'Запечатанная сила', value: '344' }, + { label: 'Застенчивые персонажи', value: '443' }, + { label: 'Звери', value: '119' }, + { label: 'Звери-компаньоны', value: '192' }, + { label: 'Злой протагонист', value: '125' }, + { label: 'Злые боги', value: '437' }, + { label: 'Злые организации', value: '503' }, + { label: 'Злые религии', value: '725' }, + { label: 'Знаменитости', value: '397' }, + { label: 'Знаменитый главный герой', value: '469' }, + { label: 'Знания современного мира', value: '185' }, + { label: 'Зомби', value: '321' }, + { label: 'Игра на выживание', value: '162' }, + { label: 'Игривый протагонист', value: '700' }, + { label: 'Игровая система рейтинга', value: '301' }, + { label: 'Игровые элементы', value: '152' }, + { label: 'Игрушки (18+)', value: '644' }, + { label: 'Из грязи в князи', value: '330' }, + { label: 'Из женщины в мужчину ', value: '688' }, + { label: 'Из мужчины в женщину', value: '698' }, + { label: 'Из полного в худого', value: '809' }, + { label: 'Из слабого в сильного', value: '81' }, + { label: 'Из страшно(го/й) в красиво(го/ю)', value: '810' }, + { label: 'Извращенный главный герой', value: '349' }, + { label: 'Изгои', value: '472' }, + { label: 'Изменение расы', value: '627' }, + { label: 'Изменения внешнего вида', value: '191' }, + { label: 'Изменения личности', value: '99' }, + { label: 'Изнасилование', value: '110' }, + { label: 'Изображения жестокости', value: '260' }, + { label: 'Империи', value: '749' }, + { label: 'Инвалидность', value: '656' }, + { label: 'Индустриализация', value: '180' }, + { label: 'Инженер', value: '35' }, + { label: 'Инцест', value: '463' }, + { label: 'Искусственный интеллект', value: '118' }, + { label: 'Исследования', value: '635' }, + { label: 'Каннибализм', value: '377' }, + { label: 'Карточные игры', value: '654' }, + { label: 'Киберспорт', value: '658' }, + { label: 'Кланы', value: '950' }, + { label: 'Класс безработного [Игровой класс в игре]', value: '727' }, + { label: 'Клоны', value: '365' }, + { label: 'Клубы', value: '96' }, + { label: 'Книги', value: '341' }, + { label: 'Книги навыков', value: '312' }, + { label: 'Книжный червь', value: '455' }, + { label: 'Коварство', value: '111' }, + { label: 'Коллеги', value: '683' }, + { label: 'Колледж/Университет', value: '712' }, + { label: 'Кома', value: '826' }, + { label: 'Командная работа', value: '426' }, + { label: 'Комедийный оттенок', value: '523' }, + { label: 'Комплекс неполноценности', value: '489' }, + { label: 'Комплекс семейных отношений', value: '104' }, + { label: 'Конкуренция', value: '746' }, + { label: 'Контракты', value: '483' }, + { label: 'Контроль разума/сознания', value: '262' }, + { label: 'Копейщик', value: '339' }, + { label: 'Королевская битва', value: '1025' }, + { label: 'Королевская власть', value: '53' }, + { label: 'Королевства', value: '141' }, + { label: 'Коррупция', value: '378' }, + { label: 'Космические войны', value: '674' }, + { label: 'Красивый герой', value: '107' }, + { label: 'Крафт', value: '287' }, + { label: 'Кризис личности', value: '599' }, + { label: 'Кругосветное путешествие', value: '257' }, + { label: 'Кудере', value: '440' }, + { label: 'Кузены', value: '701' }, + { label: 'Кузнец', value: '454' }, + { label: 'Кукловоды', value: '431' }, + { label: 'Куклы/марионетки', value: '573' }, + { label: 'Культивация', value: '123' }, + { label: 'Куннилингус', value: '632' }, + { label: 'Легенды', value: '430' }, + { label: 'Легкая жизнь', value: '604' }, + { label: 'Ленивый главный герой', value: '570' }, + { label: 'Лидерство', value: '424' }, + { label: 'Лоли', value: '92' }, + { label: 'Лотерея', value: '401' }, + { label: 'Любовный интерес влюбляется первым', value: '647' }, + { label: 'Любовный интерес главного героя носит очки', value: '576' }, + { label: 'Любовный треугольник', value: '98' }, + { label: 'Любовь детства', value: '500' }, + { label: 'Любовь с первого взгляда', value: '730' }, + { label: 'Магические надписи', value: '373' }, + { label: 'Магические печати', value: '277' }, + { label: 'Магические технологии', value: '357' }, + { + label: + 'Магическое пространство/измерение, доступное не всем персонажам', + value: '244', + }, + { label: 'Магия', value: '38' }, + { label: 'Магия призыва', value: '333' }, + { label: 'Мазохистские персонажи', value: '660' }, + { label: 'Манипулятивные персонажи', value: '130' }, + { label: 'Мания', value: '91' }, + { label: 'Мастер на все руки', value: '390' }, + { label: 'Мастурбация', value: '846' }, + { label: 'Махо-сёдзё', value: '496' }, + { label: 'Медицинские знания', value: '441' }, + { label: 'Медленная романтическая линия', value: '113' }, + { label: 'Медленное развитие на старте ', value: '670' }, + { label: 'Межпространственные путешествия', value: '316' }, + { label: 'Менеджмент', value: '182' }, + { label: 'Мертвый главный герой', value: '794' }, + { label: 'Месть', value: '88' }, + { label: 'Метаморфы', value: '234' }, + { label: 'Меч и магия', value: '55' }, + { label: 'Мечник', value: '607' }, + { label: 'Мечты', value: '733' }, + { label: 'Милая история', value: '709' }, + { label: 'Милое дитя', value: '596' }, + { label: 'Милый главный герой', value: '697' }, + { label: 'Мировое дерево', value: '385' }, + { label: 'Мистический ореол вокруг семьи', value: '409' }, + { label: 'Мифические звери', value: '418' }, + { label: 'Мифология', value: '468' }, + { label: 'Младшие братья', value: '843' }, + { label: 'Младшие сестры', value: '465' }, + { label: 'ММОРПГ (ЛитРПГ)', value: '306' }, + { label: 'Множество перемещенных людей', value: '488' }, + { label: 'Множество реальностей', value: '278' }, + { label: 'Множество реинкарнированных людей', value: '227' }, + { label: 'Модели', value: '649' }, + { label: 'Молчаливый персонаж', value: '705' }, + { label: 'Монстры', value: '69' }, + { label: 'Мужская гей-пара', value: '685' }, + { label: 'Мужчина-яндере', value: '155' }, + { label: 'Музыка', value: '589' }, + { label: 'Музыкальные группы', value: '588' }, + { label: 'Мутации', value: '668' }, + { label: 'Мутированные существа', value: '317' }, + { label: 'Навык кражи', value: '626' }, + { label: 'Навязчивая любовь', value: '85' }, + { label: 'Наемники', value: '324' }, + { label: 'Назойливый возлюбленный', value: '501' }, + { label: 'Наивный главный герой', value: '66' }, + { label: 'Наркотики', value: '661' }, + { label: 'Нарциссический главный герой', value: '470' }, + { label: 'Насилие сексуального характера', value: '824' }, + { label: 'Наследование', value: '372' }, + { label: 'Национализм', value: '318' }, + { label: 'Не блещущий внешне главный герой', value: '328' }, + { label: 'Не родные братья и сестры', value: '464' }, + { label: 'Небеса', value: '508' }, + { label: 'Небесное испытание', value: '274' }, + { label: 'Негуманоидный главный герой', value: '622' }, + { label: 'Недоверчивый главный герой', value: '485' }, + { label: 'Недооцененный главный герой', value: '140' }, + { label: 'Недоразумения', value: '202' }, + { label: 'Неизлечимая болезнь', value: '75' }, + { label: 'Некромант', value: '308' }, + { label: 'Нелинейная история', value: '157' }, + { label: 'Ненавистный главный герой', value: '487' }, + { label: 'Ненадежный рассказчик', value: '165' }, + { label: 'Нерезиденты', value: '821' }, + { label: 'Нерешительный главный герой', value: '741' }, + { label: 'Несерьезный главный герой', value: '294' }, + { label: 'Несколько временных линий', value: '517' }, + { label: 'Несколько главных героев', value: '475' }, + { label: 'Несколько идентичностей', value: '615' }, + { label: 'Несколько личностей', value: '474' }, + { label: 'Нетораре', value: '721' }, + { label: 'Нетори', value: '450' }, + { label: 'Неудачливый главный герой', value: '567' }, + { label: 'Ниндзя', value: '358' }, + { label: 'Обещание из детства', value: '738' }, + { label: 'Обманщик', value: '411' }, + { label: 'Обмен телами', value: '94' }, + { label: 'Обнаженка', value: '417' }, + { label: 'Обольщение', value: '718' }, + { label: 'Оборотни', value: '624' }, + { label: 'Обратный гарем', value: '255' }, + { label: 'Общество монстров', value: '226' }, + { label: 'Обязательство', value: '548' }, + { label: 'Огнестрельное оружие', value: '179' }, + { label: 'Ограниченная продолжительность жизни', value: '73' }, + { label: 'Одержимость', value: '447' }, + { label: 'Одинокий главный герой', value: '304' }, + { label: 'Одиночество', value: '199' }, + { label: 'Одиночное проживание', value: '207' }, + { label: 'Околосмертные переживания', value: '773' }, + { label: 'Оммёдзи', value: '542' }, + { label: 'Омоложение', value: '237' }, + { label: 'Организованная преступность', value: '478' }, + { label: 'Оргия', value: '825' }, + { label: 'Орки', value: '228' }, + { label: 'Освоение навыков', value: '235' }, + { label: 'Основано на аниме', value: '553' }, + { label: 'Основано на видео игре', value: '704' }, + { label: 'Основано на визуальной новелле ', value: '861' }, + { label: 'Основано на песне', value: '677' }, + { label: 'Основано на фильме', value: '552' }, + { label: 'Осторожный главный герой', value: '122' }, + { label: 'Отаку', value: '512' }, + { label: 'Открытый космос', value: '150' }, + { label: 'Отношения в сети', value: '713' }, + { label: 'Отношения между богом и человеком', value: '603' }, + { label: 'Отношения между людьми и нелюдьми', value: '205' }, + { label: 'Отношения на расстоянии', value: '852' }, + { label: 'Отношения начальник-подчиненный', value: '590' }, + { label: 'Отношения Сенпай-Коухай', value: '513' }, + { label: 'Отношения ученика и учителя', value: '451' }, + { label: 'Отношения учитель-ученик', value: '343' }, + { label: 'Отношения хозяин-слуга', value: '206' }, + { label: 'Отомэ игра', value: '723' }, + { label: 'Отсутствие здравого смысла', value: '605' }, + { label: 'Отсутствие родителей', value: '575' }, + { label: 'Офисный роман', value: '686' }, + { label: 'Официанты', value: '681' }, + { label: 'Охотники', value: '288' }, + { label: 'Очаровательный главный герой', value: '659' }, + { label: 'Падшее дворянство', value: '618' }, + { label: 'Падшие ангелы', value: '505' }, + { label: 'Пайзури', value: '630' }, + { label: 'Паразиты', value: '535' }, + { label: 'Параллельные миры', value: '86' }, + { label: 'Парк развлечений', value: '586' }, + { label: 'Пародия', value: '319' }, + { label: 'Певцы/Певицы', value: '734' }, + { label: 'Первая любовь', value: '716' }, + { label: 'Первоисточник новеллы — манга', value: '560' }, + { label: 'Первый раз', value: '845' }, + { + label: + 'Перемещение в другой мир, имея при себе современные достижения', + value: '732', + }, + { label: 'Перемещение в игровой мир', value: '532' }, + { label: 'Перемещение в иной мир', value: '754' }, + { label: 'Перерождение в ином мире', value: '755' }, + { label: 'Переселение души/Трансмиграция', value: '139' }, + { label: 'Персонаж использует щит', value: '631' }, + { label: 'Петля времени', value: '79' }, + { label: 'Пираты', value: '817' }, + { label: 'Писатели', value: '408' }, + { label: 'Питомцы', value: '253' }, + { label: 'Племенное общество', value: '291' }, + { label: 'Повелитель демонов', value: '171' }, + { + label: 'Повествование от нескольких лиц/Несколько точек зрения', + value: '156', + }, + { label: 'Подземелья', value: '219' }, + { label: 'Пожелания', value: '166' }, + { label: 'Познание Дао', value: '360' }, + { label: 'Покинутое дитя', value: '534' }, + { label: 'Полигамия', value: '296' }, + { label: 'Политика', value: '43' }, + { label: 'Полиция', value: '801' }, + { label: 'Полулюди', value: '335' }, + { label: 'Пользователь уникального оружия', value: '432' }, + { label: 'Популярный любовный интерес', value: '241' }, + { label: 'Постапокалиптика', value: '60' }, + { label: 'Потерянные цивилизации', value: '751' }, + { label: 'Похищения людей', value: '707' }, + { label: 'Поэзия', value: '404' }, + { label: 'Правонарушители', value: '694' }, + { label: 'Прагматичный главный герой', value: '536' }, + { label: 'Преданный любовный интерес', value: '106' }, + { label: 'Предательство', value: '103' }, + { label: 'Предвидение', value: '52' }, + { label: 'Прекрасная героиня', value: '30' }, + { label: 'Преступники', value: '728' }, + { label: 'Преступность', value: '83' }, + { label: 'Призванный герой', value: '147' }, + { label: 'Призраки', value: '51' }, + { label: 'Принуждение к отношениям', value: '602' }, + { label: 'Принцессы', value: '933' }, + { label: 'Притворная пара', value: '600' }, + { label: 'Причудливые персонажи', value: '297' }, + { label: 'Пришельцы/Инопланетяне', value: '76' }, + { label: 'Программист', value: '666' }, + { label: 'Проклятия', value: '48' }, + { label: 'Промывание мозгов', value: '519' }, + { label: 'Пропуск времени', value: '138' }, + { label: 'Пророчества', value: '429' }, + { label: 'Проститутки', value: '844' }, + { label: 'Пространственное манипулирование', value: '375' }, + { label: 'Прошлое играет большую роль', value: '215' }, + { label: 'Прыжки между мирами', value: '737' }, + { label: 'Психические силы', value: '265' }, + { label: 'Психопаты', value: '158' }, + { label: 'Путешествие во времени', value: '80' }, + { label: 'Пытка', value: '168' }, + { label: 'Рабы', value: '829' }, + { label: 'Развод', value: '771' }, + { label: 'Разумные предметы', value: '174' }, + { label: 'Расизм', value: '320' }, + { label: 'Рассказ', value: '61' }, + { label: 'Расторжения помолвки', value: '243' }, + { label: 'Расы зооморфов', value: '209' }, + { label: 'Ревность', value: '717' }, + { label: 'Редакторы', value: '684' }, + { label: 'Реинкарнация', value: '281' }, + { label: 'Реинкарнация в монстра', value: '204' }, + { label: 'Реинкарнация в объект', value: '692' }, + { label: 'Религии', value: '565' }, + { label: 'Репортеры', value: '837' }, + { label: 'Ресторан', value: '652' }, + { label: 'Решительный главный герой', value: '124' }, + { label: 'Робкий главный герой', value: '800' }, + { label: 'Родитель одиночка', value: '687' }, + { label: 'Родительский комплекс', value: '531' }, + { label: 'Родословная', value: '121' }, + { label: 'Романтический подсюжет ', value: '159' }, + { label: 'Рост персонажа', value: '95' }, + { label: 'Рыцари', value: '142' }, + { label: 'Садистские персонажи', value: '642' }, + { label: 'Самоотверженный главный герой', value: '748' }, + { label: 'Самоубийства', value: '616' }, + { label: 'Самурай', value: '646' }, + { label: 'Сборник коротких историй', value: '456' }, + { label: 'Связанные сюжетные линии', value: '473' }, + { label: 'Святые', value: '761' }, + { label: 'Священники', value: '325' }, + { label: 'Сдержанный главный герой', value: '799' }, + { label: 'Секретные организации', value: '331' }, + { label: 'Секреты', value: '577' }, + { label: 'Секс рабы', value: '830' }, + { label: 'Семейный конфликт', value: '770' }, + { label: 'Семь добродетелей', value: '233' }, + { label: 'Семь смертных грехов', value: '134' }, + { label: 'Семья', value: '251' }, + { label: 'Сёнэн-ай подсюжет ', value: '664' }, + { label: 'Серийные убийцы', value: '497' }, + { label: 'Сестринский комплекс', value: '498' }, + { label: 'Сила духа', value: '282' }, + { label: 'Сила, требующая платы за пользование', value: '520' }, + { label: 'Сильная пара', value: '109' }, + { label: 'Сильный в сильнейшего', value: '359' }, + { label: 'Сильный любовный интерес', value: '161' }, + { label: 'Синдром восьмиклассника', value: '90' }, + { label: 'Сироты', value: '214' }, + { label: 'Система уровней', value: '198' }, + { label: 'Системный администратор', value: '476' }, + { label: 'Скрытие истинной личности', value: '371' }, + { label: 'Скрытие истинных способностей', value: '252' }, + { label: 'Скрытный главный герой', value: '133' }, + { label: 'Скрытые способности', value: '128' }, + { label: 'Скульпторы', value: '366' }, + { label: 'Слабо выраженная романтическая линия', value: '213' }, + { label: 'Слабый главный герой', value: '188' }, + { label: 'Слепой главный герой', value: '864' }, + { label: 'Слуги', value: '232' }, + { label: 'Смерть', value: '49' }, + { label: 'Смерть близких', value: '361' }, + { label: 'Собственнические персонажи', value: '108' }, + { label: 'Современность', value: '40' }, + { label: 'Сожительство', value: '482' }, + { label: 'Создание армии', value: '175' }, + { label: 'Создание артефактов', value: '292' }, + { label: 'Создание клана', value: '448' }, + { label: 'Создание королевства', value: '181' }, + { label: 'Создание навыков', value: '236' }, + { label: 'Создание секты', value: '393' }, + { label: 'Солдаты/Военные', value: '71' }, + { label: 'Сон', value: '571' }, + { label: 'Состоятельные персонажи', value: '67' }, + { label: 'Социальная иерархия на основе силы', value: '137' }, + { label: 'Социальные изгои', value: '480' }, + { label: 'Спасение мира', value: '597' }, + { label: 'Специальные способности', value: '54' }, + { label: 'Спокойный главный герой', value: '32' }, + { label: 'Справедливый главный герой', value: '702' }, + { label: 'Средневековье', value: '184' }, + { label: 'Ссорящаяся пара', value: '293' }, + { label: 'Сталкеры', value: '689' }, + { label: 'Старение', value: '190' }, + { label: 'Стоические персонажи', value: '444' }, + { label: 'Стокгольмский синдром', value: '643' }, + { label: 'Стратег', value: '425' }, + { label: 'Стратегические битвы', value: '160' }, + { label: 'Стратегия', value: '1038' }, + { label: 'Стрелки', value: '458' }, + { label: 'Стрельба из лука', value: '383' }, + { label: 'Студенческий совет', value: '490' }, + { label: 'Судьба', value: '271' }, + { label: 'Суккубы', value: '484' }, + { label: 'Супер герои', value: '1039' }, + { label: 'Суровая подготовка', value: '261' }, + { label: 'Таинственная болезнь', value: '74' }, + { label: 'Таинственное прошлое', value: '263' }, + { label: 'Тайная личность', value: '310' }, + { label: 'Тайные отношения', value: '812' }, + { label: 'Танцоры', value: '840' }, + { label: 'Телохранители', value: '452' }, + { label: 'Тентакли', value: '693' }, + { label: 'Террористы', value: '515' }, + { label: 'Технологический разрыв', value: '621' }, + { label: 'Тихие персонажи', value: '546' }, + { label: 'Толстый главный герой', value: '299' }, + { label: 'Торговцы', value: '416' }, + { label: 'Травля/Буллинг', value: '89' }, + { label: 'Травник', value: '708' }, + { label: 'Трагическое прошлое', value: '164' }, + { label: 'Трансплантация воспоминаний', value: '367' }, + { label: 'Трап (Путаница с гендером персонажа)', value: '582' }, + { label: 'Трудолюбивый главный герой', value: '37' }, + { label: 'Тюрьма', value: '479' }, + { label: 'Убийства', value: '84' }, + { label: 'Убийцы', value: '248' }, + { label: 'Убийцы драконов', value: '606' }, + { label: 'Уверенный главный герой', value: '270' }, + { label: 'Удачливый главный герой', value: '402' }, + { label: 'Укротитель монстров', value: '337' }, + { label: 'Умения из прошлой жизни', value: '280' }, + { label: 'Умная пара', value: '493' }, + { label: 'Умный главный герой', value: '33' }, + { label: 'Уникальная техника Культивации', value: '254' }, + { label: 'Уникальное оружие', value: '340' }, + { label: 'Управление бизнесом', value: '315' }, + { label: 'Управление временем', value: '167' }, + { label: 'Управление кровью', value: '764' }, + { label: 'Упрямый главный герой', value: '672' }, + { label: 'Уродливый главный герой', value: '803' }, + { label: 'Ускоренный рост', value: '300' }, + { label: 'Усыновленные дети', value: '481' }, + { label: 'Усыновленный главный герой', value: '412' }, + { label: 'Уход за детьми', value: '398' }, + { label: 'Учителя', value: '345' }, + { label: 'Фамильяры', value: '541' }, + { label: 'Фанатизм', value: '613' }, + { label: 'Фантастические существа', value: '322' }, + { label: 'Фанфикшн', value: '388' }, + { label: 'Фармацевт', value: '715' }, + { label: 'Фарминг', value: '379' }, + { label: 'Феи', value: '210' }, + { label: 'Фелляция', value: '584' }, + { label: 'Фениксы', value: '374' }, + { label: 'Фетиш груди', value: '499' }, + { label: 'Философия', value: '87' }, + { label: 'Фильмы', value: '403' }, + { label: 'Флэшбэки', value: '528' }, + { label: 'Фобии', value: '100' }, + { label: 'Фольклор', value: '467' }, + { label: 'Футанари', value: '568' }, + { label: 'Футуристический сеттинг', value: '382' }, + { label: 'Фэнтези мир', value: '126' }, + { label: 'Хакеры', value: '399' }, + { label: 'Харизматический герой', value: '391' }, + { label: 'Хикикомори/Затворники', value: '462' }, + { label: 'Хитроумный главный герой', value: '105' }, + { label: 'Хозяин подземелий', value: '557' }, + { label: 'Холодный главный герой', value: '259' }, + { label: 'Хорошие отношения с семьей', value: '506' }, + { label: 'Хранители могил', value: '68' }, + { label: 'Целители', value: '389' }, + { label: 'Цзянши', value: '735' }, + { label: 'Цундэрэ', value: '445' }, + { label: 'Чаты', value: '580' }, + { label: 'Человеческое оружие', value: '521' }, + { label: 'Честный главный герой', value: '240' }, + { label: 'Читы', value: '238' }, + { label: 'Шантаж', value: '526' }, + { label: 'Шеф-повар', value: '239' }, + { label: 'Шикигами', value: '543' }, + { label: 'Школа только для девочек', value: '633' }, + { label: 'Шота', value: '514' }, + { label: 'Шоу-бизнес', value: '407' }, + { label: 'Шпионы', value: '563' }, + { label: 'Эволюция', value: '196' }, + { label: 'Эгоистичный главный герой', value: '539' }, + { label: 'Эйдетическая память', value: '392' }, + { label: 'Экзорсизм', value: '504' }, + { label: 'Экономика', value: '492' }, + { label: 'Эксгибиционизм', value: '639' }, + { label: 'Эксперименты с людьми', value: '129' }, + { label: 'Элементальная магия', value: '395' }, + { label: 'Эльфы', value: '172' }, + { label: 'Эмоционально слабый главный герой', value: '816' }, + { label: 'Эпизодический', value: '612' }, + { label: 'Юный любовный интерес', value: '446' }, + { label: 'Яды', value: '410' }, + { label: 'Языкастые персонажи', value: '406' }, + { label: 'Языковой барьер', value: '59' }, + { label: 'Яндере', value: '208' }, + { label: 'Японские силы самообороны', value: '559' }, + { label: 'Ярко выраженная романтическая линия', value: '272' }, + { label: 'Abusive Characters', value: '625' }, + { label: 'Adapted to Visual Novel', value: '1046' }, + { label: 'Adopted-lead', value: '886' }, + { label: 'Adultery', value: '866' }, + { label: 'Affair', value: '645' }, + { label: 'Age-gap', value: '923' }, + { label: 'Almost-human-lead', value: '975' }, + { label: 'An*l', value: '780' }, + { label: 'Androgynous-male-lead', value: '1002' }, + { label: 'Anti-hero-lead', value: '976' }, + { label: 'Apathetic-lead', value: '967' }, + { label: 'Arms Dealers', value: '839' }, + { label: 'Army-commander', value: '1003' }, + { label: 'Artifact-refining', value: '924' }, + { label: 'Autism', value: '855' }, + { label: 'Awkward Protagonist', value: '669' }, + { label: 'Awkward-lead', value: '892' }, + { label: 'Bestiality', value: '796' }, + { label: 'Big-breasts', value: '908' }, + { label: 'Birth-of-a-nation', value: '1004' }, + { label: 'Bisexual-lead', value: '1027' }, + { label: 'Body-refining', value: '947' }, + { label: 'Body-swap/s', value: '1036' }, + { label: 'Boss-subordinate-relationship', value: '876' }, + { label: 'Bride-kidnapping', value: '948' }, + { label: 'Caring-lead', value: '879' }, + { label: 'Cautious-lead', value: '1032' }, + { label: 'Changed-man', value: '925' }, + { label: 'Charismatic-lead', value: '1005' }, + { label: 'Child-lead', value: '936' }, + { label: 'Clever-lead', value: '880' }, + { label: 'Clumsy Love Interests', value: '815' }, + { label: 'Cold Love Interests', value: '651' }, + { label: 'Coming of Age', value: '860' }, + { label: 'Confident-lead', value: '894' }, + { label: 'Confident-male-lead', value: '951' }, + { label: 'Confinement', value: '767' }, + { label: 'Conflicting Loyalties', value: '793' }, + { label: 'Couple Growth', value: '690' }, + { label: 'Court Official', value: '863' }, + { label: 'Cowardly Protagonist', value: '703' }, + { label: 'Cowardly-lead', value: '1000' }, + { label: 'Cross-dressing', value: '250' }, + { label: 'Cunnilingus', value: '867' }, + { label: 'Cunning-lead', value: '952' }, + { label: 'Cunning-male-lead', value: '953' }, + { label: 'Curious Protagonist', value: '849' }, + { label: 'Cute-lead', value: '887' }, + { label: 'Dense-lead', value: '868' }, + { label: 'Determined-lead', value: '972' }, + { label: 'Developing-technology', value: '1006' }, + { label: 'Devil/s', value: '1007' }, + { label: 'Different Social Status', value: '758' }, + { label: 'Disfigurement', value: '838' }, + { label: 'Doting Love Interests', value: '665' }, + { label: 'Doting Older Siblings', value: '609' }, + { label: 'Doting Parents', value: '530' }, + { label: 'Dungeon/s-exploring', value: '990' }, + { label: 'Elderly Protagonist', value: '774' }, + { label: 'Enlightenment', value: '729' }, + { label: 'Eunuch', value: '1047' }, + { label: 'Eye Powers', value: '386' }, + { label: 'Family Business', value: '785' }, + { label: 'Famous Parents', value: '790' }, + { label: 'Famous-lead', value: '897' }, + { label: 'Fanfic', value: '942' }, + { label: 'Fated Lovers', value: '797' }, + { label: 'Fellatio', value: '870' }, + { label: 'Female-lead', value: '888' }, + { label: 'First-time-intercourse', value: '871' }, + { label: 'Fleet Battles', value: '836' }, + { label: 'Forced Living Arrangements', value: '808' }, + { label: 'Forced Marriage', value: '835' }, + { label: 'Former Hero', value: '752' }, + { label: 'Fujoshi', value: '1045' }, + { label: 'Galge', value: '848' }, + { label: 'Gambling', value: '1031' }, + { label: 'Gamelit', value: '994' }, + { label: 'Genderless-lead', value: '1008' }, + { label: 'Glasses-wearing-lead', value: '995' }, + { label: 'Guardian Relationship', value: '197' }, + { label: 'H*ndjob', value: '629' }, + { label: 'Half-human-lead', value: '956' }, + { label: 'Hard-working-lead', value: '957' }, + { label: 'Hard-working-male-lead', value: '958' }, + { label: 'Hard-working-protagonist/s', value: '1030' }, + { label: 'Harem-seeking-lead', value: '929' }, + { label: 'Harem-subtext', value: '909' }, + { label: 'Helpful Protagonist', value: '760' }, + { label: 'Helpful-lead', value: '882' }, + { label: 'Hidden-gem', value: '998' }, + { label: 'High-fantasy', value: '991' }, + { label: 'Human-becomes-demon/monster', value: '1009' }, + { label: 'Human-nonhuman-relationship', value: '914' }, + { label: 'Humanoid-lead', value: '915' }, + { label: 'Imperial Harem', value: '805' }, + { label: 'Incubus', value: '1049' }, + { label: 'Insects', value: '831' }, + { label: 'Interspatial-storage', value: '959' }, + { label: 'Kind Love Interests', value: '757' }, + { label: 'Large-number-of-skills', value: '1010' }, + { label: 'Legendary-hero', value: '1011' }, + { label: 'Litrpg', value: '977' }, + { label: 'Love Rivals', value: '788' }, + { label: 'Lovers Reunited', value: '853' }, + { label: 'Low-fantasy', value: '1037' }, + { label: 'Master-disciple-relationship', value: '899' }, + { label: 'Matriarchy', value: '807' }, + { label: 'Mature-lead', value: '900' }, + { label: 'Mind Break', value: '862' }, + { label: 'Mismatched Couple', value: '695' }, + { label: 'Mob Protagonist', value: '648' }, + { label: 'Mob-lead', value: '902' }, + { label: 'Modern', value: '988' }, + { label: 'Monster-pov', value: '1012' }, + { label: 'Mpreg', value: '856' }, + { label: 'Multiple-povs', value: '1013' }, + { label: 'Mystery Solving', value: '710' }, + { label: 'Naive-lead', value: '1014' }, + { label: 'Near-death-experience', value: '996' }, + { label: 'Neet', value: '495' }, + { label: 'Nightmares', value: '833' }, + { label: 'Nobility', value: '1015' }, + { label: 'Non-human-lead', value: '978' }, + { label: 'Non-humanoid-lead', value: '1016' }, + { label: 'Not-so-secret-identity', value: '979' }, + { label: 'Omegaverse', value: '857' }, + { label: 'Online-game', value: '910' }, + { label: 'Online-gaming', value: '911' }, + { label: 'Outdoor Interc**rse', value: '847' }, + { label: 'Outdoor-intercourse', value: '903' }, + { label: 'Overprotective Siblings', value: '610' }, + { label: 'Part-Time Job', value: '865' }, + { label: 'Past Trauma', value: '726' }, + { label: 'Past-memories', value: '1017' }, + { label: 'Persistent Love Interests', value: '714' }, + { label: 'Perverted-lead', value: '932' }, + { label: 'Photography', value: '798' }, + { label: 'Pill Based Cultivation', value: '279' }, + { label: 'Pill Concocting', value: '245' }, + { label: 'Pilots', value: '820' }, + { label: 'Playboys', value: '795' }, + { label: 'Playful-lead', value: '989' }, + { label: 'Polyandry', value: '811' }, + { label: 'Portal-fantasy-/-isekai', value: '992' }, + { label: 'Pragmatic-lead', value: '884' }, + { label: 'Profanity', value: '980' }, + { label: 'Protagonist-loyal-to-love-interest', value: '905' }, + { label: 'R-15 (Японское возрастное ограничение)', value: '230' }, + { label: 'Rebellion', value: '834' }, + { label: 'Reincarnated in a Game World', value: '804' }, + { label: 'Reluctant Protagonist', value: '696' }, + { label: 'Reverse R*pe', value: '776' }, + { label: 'Reversible Couple', value: '854' }, + { label: 'Ruling-class', value: '943' }, + { label: 'S*x Friends', value: '750' }, + { label: 'S*xual Cultivation Technique', value: '823' }, + { label: 'Salaryman', value: '1035' }, + { label: 'Satire', value: '981' }, + { label: 'Schemes And Conspiracies', value: '745' }, + { label: 'Scientist/s', value: '997' }, + { label: 'Scientists', value: '1029' }, + { label: 'Secret Crush', value: '827' }, + { label: 'Seme Protagonist', value: '1034' }, + { label: 'Seme-lead', value: '1028' }, + { label: 'Sentimental Protagonist', value: '1048' }, + { label: 'Seven-heavenly-virtues', value: '1018' }, + { label: 'Sexual Cultivation Technique', value: '350' }, + { label: 'Sexual-content', value: '982' }, + { label: 'Sexuality', value: '983' }, + { label: 'Sharing A Body', value: '814' }, + { label: 'Shotacon', value: '858' }, + { label: 'Shoujo-Ai Subplot', value: '777' }, + { label: 'Sibling Rivalry', value: '766' }, + { label: "Sibling's Care", value: '1042' }, + { label: 'Sickly Characters', value: '787' }, + { label: 'Skills', value: '1019' }, + { label: 'Slave-harem', value: '1001' }, + { label: 'Slime', value: '1020' }, + { label: 'Smart-male-lead', value: '963' }, + { label: 'Spirit Users', value: '739' }, + { label: 'Straight Seme', value: '819' }, + { label: 'Straight Uke', value: '818' }, + { label: 'Strength-based-social-hierarchy', value: '974' }, + { label: 'Strong-lead', value: '944' }, + { label: 'Strong-male-lead', value: '964' }, + { label: 'Student-teacher-relationship', value: '1026' }, + { label: 'Suspense', value: '987' }, + { label: 'Sword-and-sorcery', value: '875' }, + { label: 'Threesome', value: '574' }, + { label: 'Transformation Ability', value: '736' }, + { label: 'Traumatising-content', value: '1040' }, + { label: 'Underestimated-lead', value: '919' }, + { label: 'Underestimated-male-lead', value: '966' }, + { label: 'Unique-abilities', value: '1021' }, + { label: 'Unique-skill', value: '1022' }, + { label: 'Unique-skills', value: '1023' }, + { label: 'Unlimited Flow', value: '1044' }, + { label: 'Unrequited Love', value: '806' }, + { label: 'Urban-fantasy', value: '1041' }, + { label: 'Villainess Noble Girls', value: '719' }, + { label: 'Weak-lead', value: '885' }, + { label: 'Web-novel', value: '1024' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + tags: { + label: 'Жанры', + value: { include: [], exclude: [] }, + options: [ + { label: 'Боевые искусства', value: '22' }, + { label: 'Гарем', value: '114' }, + { label: 'Гендер бендер', value: '246' }, + { label: 'Дзёсэй', value: '216' }, + { label: 'Для взрослых', value: '115' }, + { label: 'Для взрослых', value: '258' }, + { label: 'Драма', value: '7' }, + { label: 'Исторический', value: '101' }, + { label: 'Комедия', value: '17' }, + { label: 'Лоликон', value: '638' }, + { label: 'Магический реализм', value: '922' }, + { label: 'Меха', value: '24' }, + { label: 'Милитари', value: '12' }, + { label: 'Мистика', value: '2' }, + { label: 'Научная фантастика', value: '13' }, + { label: 'Непристойность', value: '747' }, + { label: 'Повседневность', value: '93' }, + { label: 'Приключение', value: '11' }, + { label: 'Психология', value: '18' }, + { label: 'Романтика', value: '9' }, + { label: 'Сверхъестественное', value: '20' }, + { label: 'Сёдзё', value: '15' }, + { label: 'Сёдзё-ай', value: '23' }, + { label: 'Сёнэн', value: '189' }, + { label: 'Сёнэн-ай', value: '680' }, + { label: 'Спорт', value: '420' }, + { label: 'Сэйнэн', value: '5' }, + { label: 'Сюаньхуа', value: '242' }, + { label: 'Сянься', value: '364' }, + { label: 'Трагедия', value: '19' }, + { label: 'Триллер', value: '3' }, + { label: 'Ужасы', value: '1' }, + { label: 'Уся', value: '720' }, + { label: 'Фэнтези', value: '8' }, + { label: 'Школьная жизнь', value: '21' }, + { label: 'Экшн', value: '14' }, + { label: 'Эччи', value: '327' }, + { label: 'Юри', value: '691' }, + { label: 'Яой', value: '682' }, + { label: 'Eastern fantasy', value: '907' }, + { label: 'Isekai', value: '999' }, + { label: 'Video games', value: '993' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + } satisfies Filters; +} + +export default new RNBH(); + +type responseNovels = { + id: number; + names: Names; + rating: number; + synopsis: string; + url: string; + poster: Poster; + created_at: number; + status: string; + user?: User; + counts: Counts; +}; +type Names = { + eng?: string; + rus?: string; + original: string; +}; +type Poster = { + medium: string; + small: string; + color: string; +}; +type User = { + status?: null; + liked: boolean; +}; +type Counts = { + volumes: string; + chapters: string; +}; + +type responseNovel = { + id: number; + names: Names; + rating: number; + year: number; + synopsis: string; + url: string; + posters: Posters; + isSpecial: boolean; + liked: boolean; + authors?: AuthorsEntity[] | null; + translators?: TranslatorsEntity[] | null; + description: string; + status: Status; + start_reading_url: string; + html: string; + tags: Tags; +}; +type Posters = { + big: string; + medium: string; + small: string; + tiny: string; + color: string; +}; +type AuthorsEntity = { + name_eng: string; + pivot: Pivot; +}; +type Pivot = { + ranobe_id: number; + author_id?: number; + translator_id?: number; +}; +type TranslatorsEntity = { + name: string; + pivot: Pivot; +}; +type Status = { + id: number; + title: string; + name: string; +}; +type Tags = { + events?: GenresOrEntity[] | null; + genres?: GenresOrEntity[] | null; +}; +type GenresOrEntity = { + id: number; + names: Names; + url: string; + title: string; + description?: string | null; +}; + +type VolumesEntity = { + id: number; + num: number; + name: string; + status: Status; + chapters?: ChaptersEntity[] | null; +}; + +type ChaptersEntity = { + id: number; + name: string; + num: number; + url: string; + is_new: boolean; + has_images: boolean; + changed_at: string; + comments_count: string; +}; + +type responseSearch = { + meta: Meta; + collections?: DataEntity[] | null; + data?: DataEntity[] | null; +}; +type Meta = { + key: string; + title: string; +}; +type DataEntity = { + id: number; + names?: Names | null; + description?: string | null; + url: string; + image?: string | null; + name?: string | null; + level?: number | null; + evolution_scheme?: null; + roles?: null[] | null; + avatar?: Avatar | null; + has_plus?: boolean | null; +}; +type Avatar = { + big: string; + color: string; + thumb: string; + is_default: boolean; +}; diff --git a/plugins/russian/ranobelib.ts b/plugins/russian/ranobelib.ts new file mode 100644 index 000000000..09f9a3cb8 --- /dev/null +++ b/plugins/russian/ranobelib.ts @@ -0,0 +1,810 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { storage, localStorage } from '@libs/storage'; +import dayjs from 'dayjs'; + +const statusKey: Record<number, string> = { + 1: NovelStatus.Ongoing, + 2: NovelStatus.Completed, + 3: NovelStatus.OnHiatus, + 4: NovelStatus.Cancelled, +}; + +const BASE_HEADERS = { + Accept: 'application/json', + Referer: 'https://ranobelib.me', + Origin: 'https://ranobelib.me/', + 'Site-Id': '3', + 'client-time-zone': + Intl.DateTimeFormat().resolvedOptions().timeZone || 'Europe/Moscow', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 YaBrowser/25.12.0.0 Safari/537.36', +}; + +class RLIB implements Plugin.PluginBase { + id = 'RLIB'; + name = 'RanobeLib'; + site = 'https://ranobelib.me'; + apiSite = 'https://api.cdnlibs.org/api/manga/'; + version = '2.2.4'; + icon = 'src/ru/ranobelib/icon.png'; + webStorageUtilized = true; + imageRequestInit = { + headers: { + ...BASE_HEADERS, + Accept: + 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + }, + }; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.apiSite + '?site_id[0]=3&page=' + pageNo; + url += + '&sort_by=' + + (showLatestNovels + ? 'last_chapter_at' + : filters?.sort_by?.value || 'rating_score'); + url += '&sort_type=' + (filters?.sort_type?.value || 'desc'); + + if (filters?.require_chapters?.value) { + url += '&chapters[min]=1'; + } + if (filters?.types?.value?.length) { + url += '&types[]=' + filters.types.value.join('&types[]='); + } + if (filters?.scanlateStatus?.value?.length) { + url += + '&scanlateStatus[]=' + + filters.scanlateStatus.value.join('&scanlateStatus[]='); + } + if (filters?.manga_status?.value?.length) { + url += + '&manga_status[]=' + + filters.manga_status.value.join('&manga_status[]='); + } + + if (filters?.genres) { + if (filters.genres.value?.include?.length) { + url += '&genres[]=' + filters.genres.value.include.join('&genres[]='); + } + if (filters.genres.value?.exclude?.length) { + url += + '&genres_exclude[]=' + + filters.genres.value.exclude.join('&genres_exclude[]='); + } + } + if (filters?.tags) { + if (filters.tags.value?.include?.length) { + url += '&tags[]=' + filters.tags.value.include.join('&tags[]='); + } + if (filters.tags.value?.exclude?.length) { + url += + '&tags_exclude[]=' + + filters.tags.value.exclude.join('&tags_exclude[]='); + } + } + + const result: TopLevel = await fetchApi(url, { + headers: this.getHeaders(), + }).then(res => res.json()); + + const novels: Plugin.NovelItem[] = []; + if (result.data instanceof Array) { + result.data.forEach(novel => + novels.push({ + name: novel.rus_name || novel.eng_name || novel.name, + cover: novel.cover?.default || defaultCover, + path: novel.slug_url || novel.id + '--' + novel.slug, + }), + ); + } + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const { data }: { data: DataClass } = await fetchApi( + `${this.apiSite}${novelPath}?fields[]=summary&fields[]=genres&fields[]=tags&fields[]=teams&fields[]=authors&fields[]=status_id&fields[]=artists`, + { headers: this.getHeaders() }, + ).then(res => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: data.rus_name || data.name, + cover: data.cover?.default || defaultCover, + summary: + typeof data.summary === 'string' + ? data.summary.trim() + : data.summary?.type === 'doc' + ? jsonToHtml(data.summary.content, []) + : undefined, + }; + + if (data.status?.id) { + novel.status = statusKey[data.status.id] || NovelStatus.Unknown; + } + + if (data.authors?.length) { + novel.author = data.authors[0].name; + } + if (data.artists?.length) { + novel.artist = data.artists[0].name; + } + + const genres = [data.genres || [], data.tags || []] + .flat() + .map(genres => genres?.name) + .filter(genres => genres); + if (genres.length) { + novel.genres = genres.join(', '); + } + + const branch_name: Record<string, string> = data.teams?.reduce( + (acc, { name, details }) => { + acc[String(details?.branch_id ?? '0')] = name; + return acc; + }, + { '0': 'Главная страница' } as Record<string, string>, + ) || { '0': 'Главная страница' }; + + const chaptersJSON: { data: DataChapter[] } = await fetchApi( + `${this.apiSite}${novelPath}/chapters`, + { headers: this.getHeaders() }, + ).then(res => res.json()); + + if (chaptersJSON.data?.length) { + let chapters: Plugin.ChapterItem[] = chaptersJSON.data.flatMap(chapter => + chapter.branches.map(({ branch_id, created_at }) => { + const bId = String(branch_id ?? '0'); + return { + name: `Том ${chapter.volume} Глава ${chapter.number}${ + chapter.name ? ' ' + chapter.name.trim() : '' + }`, + path: `${novelPath}/${chapter.volume}/${chapter.number}/${bId}`, + releaseTime: created_at ? dayjs(created_at).format('LLL') : null, + chapterNumber: chapter.index, + page: branch_name[bId] || 'Неизвестный', + }; + }), + ); + + if (chapters.length) { + const uniquePages = new Set(chapters.map(c => c.page)); + + if (uniquePages.size === 1) { + // If only one unique page value, set page to undefined for all chapters + // Need more investigation one app side for single page shenanigans + chapters = chapters.map(chapter => ({ + ...chapter, + page: undefined, + })); + } else if (data.teams?.length > 1) { + // Original sorting logic for multiple pages, for reasons chapters overlap with one another + chapters.sort((chapterA, chapterB) => { + if ( + chapterA.page && + chapterB.page && + chapterA.page !== chapterB.page + ) { + return chapterA.page.localeCompare(chapterB.page); + } + return ( + (chapterA.chapterNumber || 0) - (chapterB.chapterNumber || 0) + ); + }); + } + novel.chapters = chapters; + } + } + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [slug, volume, number, branch_id] = chapterPath.split('/'); + let chapterText = ''; + + if (slug && volume && number) { + const result: { data: DataClass } = await fetchApi( + this.apiSite + + slug + + '/chapter?' + + (branch_id ? 'branch_id=' + branch_id + '&' : '') + + 'number=' + + number + + '&volume=' + + volume, + { headers: this.getHeaders() }, + ).then(res => res.json()); + const content = result?.data?.content; + chapterText = + typeof content === 'object' && content?.type === 'doc' + ? jsonToHtml(content.content, result.data.attachments || []) + : (content as string) || ''; + } + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = this.apiSite + '?site_id[0]=3&q=' + searchTerm; + const result: TopLevel = await fetchApi(url, { + headers: this.getHeaders(), + }).then(res => res.json()); + + const novels: Plugin.NovelItem[] = []; + if (result.data instanceof Array) { + result.data.forEach(novel => + novels.push({ + name: novel.rus_name || novel.eng_name || novel.name, + cover: novel.cover?.default || defaultCover, + path: novel.slug_url || novel.id + '--' + novel.slug, + }), + ); + } + + return novels; + } + + resolveUrl = (path: string, isNovel?: boolean) => { + const ui = this.user?.ui ? 'ui=' + this.user.ui : ''; + + if (isNovel) return this.site + '/ru/book/' + path + (ui ? '?' + ui : ''); + + const [slug, volume, number, branch_id] = path.split('/'); + const chapterPath = + slug + + '/read/v' + + volume + + '/c' + + number + + (branch_id ? '?bid=' + branch_id : ''); + + return ( + this.site + + '/ru/' + + chapterPath + + (ui ? (branch_id ? '&' : '?') + ui : '') + ); + }; + + getUser = () => { + const user = storage.get('user') as + | { token: string; id: number } + | undefined; + if (user) { + return { token: { Authorization: 'Bearer ' + user.token }, ui: user.id }; + } + const dataRaw = localStorage.get()?.auth; + if (!dataRaw) { + return {}; + } + + const data = JSON.parse(dataRaw) as authorization; + if (!data?.token?.access_token) return; + storage.set( + 'user', + { + id: data.auth.id, + token: data.token.access_token, + }, + data.token.timestamp + data.token.expires_in, //the token is valid for about 7 days + ); + return { + token: { Authorization: 'Bearer ' + data.token.access_token }, + ui: data.auth.id, + }; + }; + + protected getHeaders() { + const apiUrl = new URL(this.apiSite); + + return { + ...BASE_HEADERS, + Host: apiUrl.host, + ...(this.user?.token || {}), + }; + } + + user = this.getUser(); //To change the account, you need to restart the application + + filters = { + sort_by: { + label: 'Сортировка', + value: 'rating_score', + options: [ + { label: 'По рейтингу', value: 'rate_avg' }, + { label: 'По популярности', value: 'rating_score' }, + { label: 'По просмотрам', value: 'views' }, + { label: 'Количеству глав', value: 'chap_count' }, + { label: 'Дате обновления', value: 'last_chapter_at' }, + { label: 'Дате добавления', value: 'created_at' }, + { label: 'По названию (A-Z)', value: 'name' }, + { label: 'По названию (А-Я)', value: 'rus_name' }, + ], + type: FilterTypes.Picker, + }, + sort_type: { + label: 'Порядок', + value: 'desc', + options: [ + { label: 'По убыванию', value: 'desc' }, + { label: 'По возрастанию', value: 'asc' }, + ], + type: FilterTypes.Picker, + }, + types: { + label: 'Тип', + value: [], + options: [ + { label: 'Япония', value: '10' }, + { label: 'Корея', value: '11' }, + { label: 'Китай', value: '12' }, + { label: 'Английский', value: '13' }, + { label: 'Авторский', value: '14' }, + { label: 'Фанфик', value: '15' }, + ], + type: FilterTypes.CheckboxGroup, + }, + scanlateStatus: { + label: 'Статус перевода', + value: [], + options: [ + { label: 'Продолжается', value: '1' }, + { label: 'Завершен', value: '2' }, + { label: 'Заморожен', value: '3' }, + { label: 'Заброшен', value: '4' }, + ], + type: FilterTypes.CheckboxGroup, + }, + manga_status: { + label: 'Статус тайтла', + value: [], + options: [ + { label: 'Онгоинг', value: '1' }, + { label: 'Завершён', value: '2' }, + { label: 'Анонс', value: '3' }, + { label: 'Приостановлен', value: '4' }, + { label: 'Выпуск прекращён', value: '5' }, + ], + type: FilterTypes.CheckboxGroup, + }, + genres: { + label: 'Жанры', + value: { include: [], exclude: [] }, + options: [ + { label: 'Арт', value: '32' }, + { label: 'Безумие', value: '91' }, + { label: 'Боевик', value: '34' }, + { label: 'Боевые искусства', value: '35' }, + { label: 'Вампиры', value: '36' }, + { label: 'Военное', value: '89' }, + { label: 'Гарем', value: '37' }, + { label: 'Гендерная интрига', value: '38' }, + { label: 'Героическое фэнтези', value: '39' }, + { label: 'Демоны', value: '81' }, + { label: 'Детектив', value: '40' }, + { label: 'Детское', value: '88' }, + { label: 'Дзёсэй', value: '41' }, + { label: 'Драма', value: '43' }, + { label: 'Игра', value: '44' }, + { label: 'Исекай', value: '79' }, + { label: 'История', value: '45' }, + { label: 'Киберпанк', value: '46' }, + { label: 'Кодомо', value: '76' }, + { label: 'Комедия', value: '47' }, + { label: 'Космос', value: '83' }, + { label: 'Магия', value: '85' }, + { label: 'Махо-сёдзё', value: '48' }, + { label: 'Машины', value: '90' }, + { label: 'Меха', value: '49' }, + { label: 'Мистика', value: '50' }, + { label: 'Музыка', value: '80' }, + { label: 'Научная фантастика', value: '51' }, + { label: 'Омегаверс', value: '77' }, + { label: 'Пародия', value: '86' }, + { label: 'Повседневность', value: '52' }, + { label: 'Полиция', value: '82' }, + { label: 'Постапокалиптика', value: '53' }, + { label: 'Приключения', value: '54' }, + { label: 'Психология', value: '55' }, + { label: 'Романтика', value: '56' }, + { label: 'Самурайский боевик', value: '57' }, + { label: 'Сверхъестественное', value: '58' }, + { label: 'Сёдзё', value: '59' }, + { label: 'Сёдзё-ай', value: '60' }, + { label: 'Сёнэн', value: '61' }, + { label: 'Сёнэн-ай', value: '62' }, + { label: 'Спорт', value: '63' }, + { label: 'Супер сила', value: '87' }, + { label: 'Сэйнэн', value: '64' }, + { label: 'Трагедия', value: '65' }, + { label: 'Триллер', value: '66' }, + { label: 'Ужасы', value: '67' }, + { label: 'Фантастика', value: '68' }, + { label: 'Фэнтези', value: '69' }, + { label: 'Хентай', value: '84' }, + { label: 'Школа', value: '70' }, + { label: 'Эротика', value: '71' }, + { label: 'Этти', value: '72' }, + { label: 'Юри', value: '73' }, + { label: 'Яой', value: '74' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + tags: { + label: 'Теги', + value: { include: [], exclude: [] }, + options: [ + { label: 'Авантюристы', value: '328' }, + { label: 'Антигерой', value: '175' }, + { label: 'Бессмертные', value: '333' }, + { label: 'Боги', value: '218' }, + { label: 'Борьба за власть', value: '309' }, + { label: 'Брат и сестра', value: '360' }, + { label: 'Ведьма', value: '339' }, + { label: 'Видеоигры', value: '204' }, + { label: 'Виртуальная реальность', value: '214' }, + { label: 'Владыка демонов', value: '349' }, + { label: 'Военные', value: '198' }, + { label: 'Воспоминания из другого мира', value: '310' }, + { label: 'Выживание', value: '212' }, + { label: 'ГГ женщина', value: '294' }, + { label: 'ГГ имба', value: '292' }, + { label: 'ГГ мужчина', value: '295' }, + { label: 'ГГ не ояш', value: '325' }, + { label: 'ГГ не человек', value: '331' }, + { label: 'ГГ ояш', value: '326' }, + { label: 'Главный герой бог', value: '324' }, + { label: 'Глупый ГГ', value: '298' }, + { label: 'Горничные', value: '171' }, + { label: 'Гуро', value: '306' }, + { label: 'Гяру', value: '197' }, + { label: 'Демоны', value: '157' }, + { label: 'Драконы', value: '313' }, + { label: 'Древний мир', value: '317' }, + { label: 'Зверолюди', value: '163' }, + { label: 'Зомби', value: '155' }, + { label: 'Исторические фигуры', value: '323' }, + { label: 'Кулинария', value: '158' }, + { label: 'Культивация', value: '161' }, + { label: 'ЛГБТ', value: '344' }, + { label: 'ЛитРПГ', value: '319' }, + { label: 'Лоли', value: '206' }, + { label: 'Магия', value: '170' }, + { label: 'Машинный перевод', value: '345' }, + { label: 'Медицина', value: '159' }, + { label: 'Межгалактическая война', value: '330' }, + { label: 'Монстр Девушки', value: '207' }, + { label: 'Монстры', value: '208' }, + { label: 'Мрачный мир', value: '316' }, + { label: 'Музыка', value: '358' }, + { label: 'Музыка', value: '209' }, + { label: 'Ниндзя', value: '199' }, + { label: 'Обратный Гарем', value: '210' }, + { label: 'Офисные Работники', value: '200' }, + { label: 'Пираты', value: '341' }, + { label: 'Подземелья', value: '314' }, + { label: 'Политика', value: '311' }, + { label: 'Полиция', value: '201' }, + { label: 'Преступники / Криминал', value: '205' }, + { label: 'Призраки / Духи', value: '196' }, + { label: 'Призыватели', value: '329' }, + { label: 'Прыжки между мирами', value: '321' }, + { label: 'Путешествие в другой мир', value: '318' }, + { label: 'Путешествие во времени', value: '213' }, + { label: 'Рабы', value: '355' }, + { label: 'Ранги силы', value: '312' }, + { label: 'Реинкарнация', value: '154' }, + { label: 'Самураи', value: '202' }, + { label: 'Скрытие личности', value: '315' }, + { label: 'Средневековье', value: '174' }, + { label: 'Традиционные игры', value: '203' }, + { label: 'Умный ГГ', value: '303' }, + { label: 'Характерный рост', value: '332' }, + { label: 'Хикикомори', value: '167' }, + { label: 'Эволюция', value: '322' }, + { label: 'Элементы РПГ', value: '327' }, + { label: 'Эльфы', value: '217' }, + { label: 'Якудза', value: '165' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + require_chapters: { + label: 'Только проекты с главами', + value: true, + type: FilterTypes.Switch, + }, + } satisfies Filters; +} + +export default new RLIB(); + +function jsonToHtml(json: HTML[], images: Attachment[], html = '') { + json.forEach(element => { + switch (element.type) { + case 'hardBreak': + html += '<br>'; + break; + case 'horizontalRule': + html += '<hr>'; + break; + case 'image': + if (element.attrs?.images?.length) { + element.attrs.images.forEach( + ({ image }: { image: string | number }) => { + const file = images.find( + (f: Attachment) => f.name == image || f.id == image, + ); + if (file) { + html += `<img src='${file.url}'>`; + } + }, + ); + } else if (element.attrs) { + const attrs = Object.entries(element.attrs) + .filter(attr => attr?.[1]) + .map(attr => `${attr[0]}="${attr[1]}"`); + html += '<img ' + attrs.join('; ') + '>'; + } + break; + case 'paragraph': + html += + '<p>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</p>'; + break; + case 'orderedList': + html += + '<ol>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</ol>'; + break; + case 'listItem': + html += + '<li>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</li>'; + break; + case 'blockquote': + html += + '<blockquote>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</blockquote>'; + break; + case 'italic': + html += + '<i>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</i>'; + break; + case 'bold': + html += + '<b>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</b>'; + break; + case 'underline': + html += + '<u>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</u>'; + break; + case 'heading': + html += + '<h2>' + + (element.content ? jsonToHtml(element.content, images) : '<br>') + + '</h2>'; + break; + case 'text': + html += element.text; + break; + default: + html += JSON.stringify(element, null, '\t'); //maybe I missed something. + break; + } + }); + return html; +} + +type HTML = { + type: string; + content?: HTML[]; + attrs?: Attrs; + text?: string; +}; + +type Attrs = { + src?: string; + alt?: string | null; + title?: string | null; + images?: { image: string | number }[]; +}; + +type authorization = { + token: Token; + auth: Auth; + timestamp: number; +}; +type Token = { + token_type: string; + expires_in: number; + access_token: string; + refresh_token: string; + timestamp: number; +}; +type Auth = { + id: number; + username: string; + avatar: Cover; + last_online_at: string; + metadata: Metadata; +}; +type Metadata = { + auth_domains: string; +}; + +type TopLevel = { + data: DataClass | DataClass[]; + links?: Links; + meta?: Meta; +}; + +type AgeRestriction = { + id: number; + label: string; +}; + +type Branch = { + id: number; + branch_id: number | null; + created_at: string; + teams: BranchTeam[]; + user: User; +}; + +type BranchTeam = { + id: number; + slug: string; + slug_url: string; + model: string; + name: string; + cover: Cover; +}; + +type Cover = { + filename: null | string; + thumbnail: string; + default: string; +}; + +type User = { + username: string; + id: number; +}; + +type DataClass = { + id: number; + name: string; + rus_name?: string; + eng_name?: string; + slug: string; + slug_url?: string; + cover?: Cover; + ageRestriction?: AgeRestriction; + site?: number; + type: string; + summary?: string | { type: string; content: HTML[] }; + is_licensed?: boolean; + teams: DataTeam[]; + genres?: Genre[]; + tags?: Genre[]; + authors?: Artist[]; + model?: string; + status?: AgeRestriction; + scanlateStatus?: AgeRestriction; + artists?: Artist[]; + releaseDateString?: string; + volume?: string; + number?: string; + number_secondary?: string; + branch_id?: null; + manga_id?: number; + created_at?: string; + moderated?: AgeRestriction; + likes_count?: number; + content?: string | { type: string; content: HTML[] }; + attachments?: Attachment[]; +}; + +type Artist = { + id: number; + slug: string; + slug_url: string; + model: string; + name: string; + rus_name: null; + alt_name: null; + cover: Cover; + subscription: Subscription; + confirmed: null; + user_id: number; + titles_count_details: null; +}; + +type Subscription = { + is_subscribed: boolean; + source_type: string; + source_id: number; + relation: null; +}; + +type Attachment = { + id?: string | null; // Allow null for id + filename: string; + name: string; + extension: string; + url: string; + width: number; + height: number; +}; + +type Genre = { + id: number; + name: string; +}; + +type DataTeam = { + id: number; + slug: string; + slug_url: string; + model: string; + name: string; + cover: Cover; + details?: Details; + vk?: string; + discord?: null; +}; + +type Details = { + branch_id: null; + is_active: boolean; + subscriptions_count: null; +}; + +type Links = { + first: string; + last: null; + prev: null; + next: string; +}; + +type Meta = { + current_page?: number; + from?: number; + path?: string; + per_page?: number; + to?: number; + page?: number; + has_next_page?: boolean; + seed?: string; + country?: string; +}; + +type DataChapter = { + id: number; + index: number; + item_number: number; + volume: string; + number: string; + number_secondary: string; + name: string; + branches_count: number; + branches: Branch[]; +}; diff --git a/plugins/russian/ranoberf.ts b/plugins/russian/ranoberf.ts new file mode 100644 index 000000000..e5d557fd2 --- /dev/null +++ b/plugins/russian/ranoberf.ts @@ -0,0 +1,405 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +const regex = + /<script id="__NEXT_DATA__" type="application\/json">(\{.*?\})<\/script>/; + +class RNRF implements Plugin.PluginBase { + id = 'RNRF'; + name = 'РанобэРФ'; + site = 'https://ранобэ.рф'; + version = '1.0.1'; + icon = 'src/ru/ranoberf/icon.png'; + + async popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/books?order='; + url += showLatestNovels + ? 'lastPublishedChapter' + : filters?.sort?.value || 'popular'; + url += '&page=' + pageNo; + + const body = await fetchApi(url).then(res => res.text()); + const novels: Plugin.NovelItem[] = []; + + const jsonRaw = body.match(regex); + if (jsonRaw instanceof Array && jsonRaw[1]) { + const json: response = JSON.parse(jsonRaw[1]); + + json.props.pageProps?.totalData?.items?.forEach(novel => + novels.push({ + name: novel.title, + cover: novel?.verticalImage?.url + ? this.site + novel.verticalImage.url + : defaultCover, + path: '/' + novel.slug, + }), + ); + } + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(res => res.text()); + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + }; + + const jsonRaw = body.match(regex); + if (jsonRaw instanceof Array && jsonRaw[1]) { + const book: PagePropsBook = JSON.parse(jsonRaw[1]).props.pageProps.book; + + novel.name = book.title; + novel.summary = book.description; + + novel.cover = book.verticalImage?.url + ? this.site + book.verticalImage.url + : defaultCover; + novel.status = book?.additionalInfo.includes('Активен') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + + if (book.author) novel.author = book.author; + if (book.genres?.length) + novel.genres = book?.genres.map(item => item.title).join(','); + + if (book.chapters?.length) { + const chapters: Plugin.ChapterItem[] = []; + book.chapters.forEach((chapter, chapterIndex) => { + if (!chapter.isDonate || chapter.isUserPaid) { + chapters.push({ + name: chapter.title, + path: chapter.url, + releaseTime: dayjs(chapter.publishedAt).format('LLL'), + chapterNumber: book.chapters.length - chapterIndex, + }); + } + }); + + novel.chapters = chapters.reverse(); + } + } + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(res => + res.text(), + ); + + const jsonRaw = body.match(regex); + if (jsonRaw instanceof Array && jsonRaw[1]) { + const chapter: string = + JSON.parse(jsonRaw[1])?.props?.pageProps?.chapter?.content?.text || ''; + + return chapter; + } + + return ''; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/v3/books?filter[or][0][title][like]=${searchTerm}&filter[or][1][titleEn][like]=${searchTerm}&filter[or][2][fullTitle][like]=${searchTerm}&filter[status][]=active&filter[status][]=abandoned&filter[status][]=completed&expand=verticalImage`; + const { items }: { items: Item[] } = await fetchApi(url).then(res => + res.json(), + ); + const novels: Plugin.NovelItem[] = []; + + items.forEach(novel => + novels.push({ + name: novel.title, + cover: novel?.verticalImage?.url + ? this.site + novel.verticalImage.url + : defaultCover, + path: '/' + novel.slug, + }), + ); + + return novels; + } + + filters = { + sort: { + label: 'Сортировка', + value: 'popular', + options: [ + { label: 'Рейтинг', value: 'popular' }, + { label: 'Дате добавления', value: 'new' }, + { label: 'Дате обновления', value: 'lastPublishedChapter' }, + { label: 'Законченные', value: 'completed' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RNRF(); + +type response = { + props: Props; + page: string; + query: Query; + buildId: string; + isFallback: boolean; + gssp: boolean; + appGip: boolean; + dynamicIds?: string[]; +}; + +type Props = { + pageProps: PageProps; + isMobile: boolean; + theme: string; + __N_SSP: boolean; +}; + +type PageProps = { + initialReduxState: InitialReduxState; + book?: PagePropsBook; + totalData?: TotalData; + layoutViewCookies?: string; + displayBanner?: boolean; + chapter?: PagePropsChapter; +}; + +type PagePropsBook = { + id: number; + countryId: number; + creatorId: number; + lastChapterId: number; + sourceId: number; + imgVerticalId: number; + imgHorizontalId: number; + imgSquareId: number; + imageVerticalId: number; + imageHorizontalId: number; + title: string; + titleEn: string; + fullTitle: string; + fullTitleEn: string; + description: string; + slug: string; + author: string; + additionalInfo: string; + importantInfo: string; + status: string; + views: number; + likes: number; + dislikes: number; + intervalSec: number; + intervalForNextFreeChapterSec: number; + chapterCost: number; + chapterPrice: number; + chapterPriceCollected: number; + maxSubscriptionChapters: number; + maxDonateChapters: number; + isDisplayOnMainPage: boolean; + withoutSchedule: boolean; + nextFreeChapterPublishedAt: null; + createdAt: Date; + updatedAt: Date; + userRating: null; + userBookmark: null; + country: Country; + genres: Genre[]; + timer: Timer; + chapters: ChapterElement[]; + verticalImage: Image; + horizontalImage: Image; + squareImage: Image; +}; + +type ChapterElement = { + id: number; + bookId: number; + title: string; + url: string; + numberChapter: string; + tom: number | null; + chapterShortNumber: number | null; + isDonate: boolean; + isSponsored: boolean; + isSubscription: boolean; + isEdited: boolean; + publishedAt: Date; + price: number; + views: number; + isUserPaid?: boolean; +}; + +type Country = { + id: number; + title: string; + code: string; +}; + +type Genre = { + id: number; + title: string; + slug: string; +}; + +type Image = { + id: number; + alt: string; + path: string; + name: string; + url: string; +}; + +type Timer = { + value: null; + for: null; + message: string; +}; + +type PagePropsChapter = { + id: number; + bookId: number; + editorId: null; + translatorId: number; + parserChapterId: null; + title: string; + slug: string; + status: string; + tom: null; + views: number; + isSponsored: boolean; + isSubscription: boolean; + isDonate: boolean; + isIndexing: boolean; + publishedAt: Date; + changeAvailabilityAt: null; + createdAt: Date; + updatedAt: Date; + numberChapter: string; + price: number; + content: Content; + book: ChapterBook; + userBookmark: null; + isUserPaid: boolean; + nextChapter: null; + previousChapter: PreviousChapter; + corrections: null; +}; + +type ChapterBook = { + id: number; + title: string; + titleEn: string; + url: string; +}; + +type Content = { + id: number; + text: string; + symbolsCount: number; +}; + +type PreviousChapter = { + id: number; + title: string; + views: number; + publishedAt: Date; + url: string; + numberChapter: string; +}; + +type InitialReduxState = { + data?: Data; + auth: Auth; + banners?: Banners; + corrections?: Corrections; + readerSettings?: ReaderSettings; +}; + +type Auth = { + isAuth: boolean; + user: null; +}; + +type Banners = { + displaySale: boolean; +}; + +type Corrections = { + items: null; + data: null; +}; + +type Data = { + bookPage: BookPage | null; + chapterPage: ChapterPage | null; +}; + +type BookPage = { + bookId: number; + bookmark: null; +}; + +type ChapterPage = { + chapterId: number; + bookId: number; + bookmark: null; +}; + +type ReaderSettings = { + fontSize: number; + lineHeight: number; + textIndent: number; + sidebarHide: number; + textAlign: string; +}; + +type TotalData = { + items: Item[]; + _links: Links; + pagesData: PagesData; +}; + +type Links = { + self: First; + first: First; + last: First; + next: First; +}; + +type First = { + href: string; +}; + +type Item = { + id: number; + title: string; + description: string; + slug: string; + url: string; + status: string; + dislikes: number; + likes: number; + userRating: null; + lastChapter: ChapterElement; + verticalImage: Image; + horizontalImage: Image; + squareImage: Image; +}; + +type PagesData = { + totalCount: number; + pageCount: number; + currentPage: number; + perPage: number; +}; + +type Query = { + book?: string; + chapter?: string; +}; diff --git a/plugins/russian/renovels.ts b/plugins/russian/renovels.ts new file mode 100644 index 000000000..06e2b9c11 --- /dev/null +++ b/plugins/russian/renovels.ts @@ -0,0 +1,1085 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + +class ReN implements Plugin.PluginBase { + id = 'ReN'; + name = 'Renovels'; + icon = 'src/ru/renovels/icon.png'; + site = 'https://renovels.org'; + version = '1.0.4'; + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams({ + count: '30', + ordering: '-' + (showLatestNovels ? 'chapter_date' : filters.sort.value), + }); + + for (const [type, filter] of Object.entries(filters)) { + const value = filter.value; + if (Array.isArray(value)) { + value.forEach(v => params.append(type, v)); + } else if (typeof value === 'object' && value !== null) { + value.include?.forEach((v: string) => params.append(type, v)); + value.exclude?.forEach((v: string) => + params.append(`exclude_${type}`, v), + ); + } + } + + params.append('page', pageNo.toString()); + const url = `${this.site}/api/v2/search/catalog/?${params.toString()}`; + + const { results }: { results: responseNovels[] } = await fetchApi(url).then( + res => res.json(), + ); + const novels: Plugin.NovelItem[] = results.map(novel => ({ + name: novel.main_name || novel.secondary_name, + cover: + this.site + (novel.cover.high || novel.cover.mid || novel.cover.low), + path: novel.dir, + })); + + return novels; + } + + async parseNovel(novelID: string): Promise<Plugin.SourceNovel> { + const { content }: { content: responseNovel } = await fetchApi( + this.site + '/api/titles/' + novelID, + ).then(res => res.json()); + + const novel: Plugin.SourceNovel = { + path: novelID, + name: content.main_name || content.secondary_name || content.another_name, + summary: content.description, + cover: + this.site + + '/media/' + + (content.cover.high || content.cover.mid || content.cover.low), + status: + content.status.name === 'Продолжается' + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + const tags = [content?.genres, content?.categories] + .flat() + .map(tags => tags?.name) + .filter(tags => tags); + + if (tags.length) { + novel.genres = tags.join(','); + } + + const totalPages = + (content.branches?.[0]?.count_chapters || content.count_chapters) / 100; + const branch_id = content.branches?.[0]?.id || content.id; + const chapters: Plugin.ChapterItem[] = []; + + for (let page = 0; page < totalPages; page++) { + await delay(1000); + const volumes: { + content: responseСhapters[]; + } = await fetchApi( + this.site + + '/api/titles/chapters/?branch_id=' + + branch_id + + '&ordering=index&user_data=1&count=100&page=' + + (page + 1), + ).then(res => res.json()); + let skip = false; + + volumes.content.forEach(chapter => { + if (!chapter.is_paid || chapter.is_bought) { + chapters.push({ + name: + 'Том ' + + chapter.tome + + ' Глава ' + + chapter.chapter + + (chapter.name ? ' ' + chapter.name.trim() : ''), + path: novelID + '/' + chapter.id, + releaseTime: dayjs(chapter.upload_date).format('LLL'), + chapterNumber: chapter.index, + }); + } else { + skip = true; + } + }); + if (skip) break; + } + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const url = + this.site + '/api/v2/titles/chapters/' + chapterPath.split('/')[1]; + const result: responseСhapter = await fetchApi(url).then(res => res.json()); + + return result.content || ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number | undefined = 1, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/api/v2/search/?query=${searchTerm}&count=100&field=titles&page=${pageNo}`; + const { results }: { results: responseNovels[] } = await fetchApi(url).then( + res => res.json(), + ); + const novels: Plugin.NovelItem[] = []; + + results.forEach(novel => + novels.push({ + name: novel.main_name || novel.secondary_name, + cover: + this.site + (novel.cover.high || novel.cover.mid || novel.cover.low), + path: novel.dir, + }), + ); + + return novels; + } + + resolveUrl = (path: string) => this.site + '/novel/' + path; + + filters = { + sort: { + label: 'Сортировка', + value: 'rating', + options: [ + { label: 'Рейтинг', value: 'rating' }, + { label: 'Просмотры', value: 'views' }, + { label: 'Лайкам', value: 'votes' }, + { label: 'Дате добавления', value: 'id' }, + { label: 'Дате обновления', value: 'chapter_date' }, + { label: 'Количество глав', value: 'count_chapters' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Жанры:', + value: { include: [], exclude: [] }, + options: [ + { label: 'Боевик', value: '112' }, + { label: 'Война', value: '123' }, + { label: 'Детектив', value: '114' }, + { label: 'Драма', value: '125' }, + { label: 'Историческая проза', value: '115' }, + { label: 'ЛитРПГ', value: '109' }, + { label: 'Любовные романы', value: '116' }, + { label: 'Мистика', value: '103' }, + { label: 'Научная фантастика', value: '121' }, + { label: 'Повседневность', value: '128' }, + { label: 'Подростковая проза', value: '113' }, + { label: 'Политический роман', value: '119' }, + { label: 'Попаданцы', value: '108' }, + { label: 'Поэзия', value: '105' }, + { label: 'Разное', value: '111' }, + { label: 'РеалРПГ', value: '117' }, + { label: 'Романтика', value: '126' }, + { label: 'Современная проза', value: '102' }, + { label: 'Современная фантастика', value: '127' }, + { label: 'Спорт', value: '122' }, + { label: 'Трагедия', value: '129' }, + { label: 'Триллер', value: '110' }, + { label: 'Триллер и саспенс', value: '124' }, + { label: 'Ужасы', value: '106' }, + { label: 'Фантастика', value: '101' }, + { label: 'Фанфик', value: '107' }, + { label: 'Фьюжн роман', value: '120' }, + { label: 'Фэнтези', value: '100' }, + { label: 'Эротика', value: '118' }, + { label: 'Юмор', value: '104' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + categories: { + label: 'Тэги:', + value: { include: [], exclude: [] }, + options: [ + { label: '[Награжденная работа]', value: '648' }, + { label: '18+', value: '423' }, + { label: 'Абьюзеры', value: '663' }, + { label: 'Авантюристы', value: '436' }, + { label: 'Автоматоны', value: '593' }, + { label: 'Агрессивные персонажи', value: '502' }, + { label: 'Ад', value: '569' }, + { label: 'Адаптация в радиопостановку', value: '581' }, + { label: 'Академия', value: '153' }, + { label: 'Актеры озвучки', value: '627' }, + { label: 'Активный главный герой', value: '251' }, + { label: 'Алхимия', value: '233' }, + { label: 'Альтернативная история', value: '147' }, + { label: 'Альтернативный мир', value: '156' }, + { label: 'Амнезия/Потеря памяти', value: '344' }, + { label: 'Анабиоз', value: '685' }, + { label: 'Ангелы', value: '183' }, + { label: 'Андрогинные персонажи', value: '321' }, + { label: 'Андроиды', value: '203' }, + { label: 'Анти-магия', value: '533' }, + { label: 'Антигерой', value: '432' }, + { label: 'Антикварный магазин', value: '623' }, + { label: 'Антисоциальный главный герой', value: '613' }, + { label: 'Антиутопия', value: '142' }, + { label: 'Апатичный протагонист', value: '157' }, + { label: 'Апокалипсис', value: '402' }, + { label: 'Аранжированный брак', value: '375' }, + { label: 'Армия', value: '639' }, + { label: 'Артефакты', value: '234' }, + { label: 'Артисты', value: '523' }, + { label: 'Банды', value: '630' }, + { label: 'БДСМ', value: '699' }, + { label: 'Бедный главный герой', value: '398' }, + { label: 'Безжалостный главный герой', value: '261' }, + { label: 'Беззаботный главный герой', value: '438' }, + { label: 'Безусловная любовь', value: '679' }, + { label: 'Беременность', value: '250' }, + { label: 'Бесполый главный герой', value: '325' }, + { label: 'Бессмертные', value: '368' }, + { label: 'Бесстрашный протагонист', value: '656' }, + { label: 'Бесстыдный главный герой', value: '352' }, + { label: 'Бесчестный главный герой', value: '716' }, + { label: 'Библиотека', value: '428' }, + { label: 'Бизнес-литература', value: '121' }, + { label: 'Бизнесмен', value: '769' }, + { label: 'Биочип', value: '238' }, + { label: 'Бисексуальный главный герой', value: '773' }, + { label: 'Близнецы', value: '263' }, + { label: 'Боги', value: '317' }, + { label: 'Богини', value: '439' }, + { label: 'Боевая академия', value: '450' }, + { label: 'Боевая фантастика', value: '149' }, + { label: 'Боевое фэнтези', value: '104' }, + { label: 'Боевые духи', value: '433' }, + { label: 'Боевые соревнования', value: '493' }, + { label: 'Божественная защита', value: '421' }, + { label: 'Божественные силы', value: '327' }, + { label: 'Борьба за власть', value: '599' }, + { label: 'Брак', value: '446' }, + { label: 'Брак по расчету', value: '188' }, + { label: 'Братский комплекс', value: '159' }, + { label: 'Братство', value: '487' }, + { label: 'Братья и сестры', value: '577' }, + { label: 'Буддизм', value: '745' }, + { label: 'Быстрая культивация', value: '366' }, + { label: 'Быстрообучаемый', value: '324' }, + { label: 'Валькирии', value: '692' }, + { label: 'Вампиры', value: '360' }, + { label: 'Ваншот', value: '702' }, + { label: 'Ведьмы', value: '281' }, + { label: 'Вежливый главный герой', value: '379' }, + { label: 'Верные подчиненные', value: '328' }, + { label: 'Взрослый главный герой', value: '295' }, + { label: 'Видит то, чего не видят другие', value: '671' }, + { label: 'Виртуальная реальность', value: '401' }, + { label: 'Владелец магазина', value: '681' }, + { label: 'Внезапная сила', value: '455' }, + { label: 'Внезапное богатство', value: '765' }, + { label: 'Внешний вид отличается от факт', value: '419' }, + { label: 'Военные Летописи', value: '743' }, + { label: 'Возвращение из другого мира', value: '696' }, + { label: 'Войны', value: '181' }, + { label: 'Вокалоид', value: '701' }, + { label: 'Волшебники/Волшебницы', value: '539' }, + { label: 'Волшебные звери', value: '308' }, + { label: 'Воображаемый друг', value: '651' }, + { label: 'Воры', value: '414' }, + { label: 'Воскрешение', value: '199' }, + { label: 'Враги становятся возлюбленными', value: '498' }, + { label: 'Враги становятся союзниками', value: '562' }, + { label: 'Врата в другой мир', value: '609' }, + { label: 'Врачи', value: '376' }, + { label: 'Временной парадокс', value: '275' }, + { label: 'Всемогущий главный герой', value: '169' }, + { label: 'Вторжение на землю', value: '198' }, + { label: 'Второй шанс', value: '231' }, + { label: 'Вуайеризм', value: '790' }, + { label: 'Выживание', value: '380' }, + { label: 'Высокомерные персонажи', value: '361' }, + { label: 'Гадание', value: '595' }, + { label: 'Гарем рабов', value: '777' }, + { label: 'Гг - женщина', value: '186' }, + { label: 'Гг - мужчина', value: '166' }, + { label: 'Гг силен с самого начала', value: '171' }, + { label: 'Геймеры', value: '392' }, + { label: 'Генералы', value: '326' }, + { label: 'Генетические модификации', value: '657' }, + { label: 'Гениальный главный герой', value: '617' }, + { label: 'Герои', value: '285' }, + { label: 'Героиня — сорванец', value: '583' }, + { label: 'Героическая фантастика', value: '140' }, + { label: 'Героическое фэнтези', value: '107' }, + { label: 'Герой влюбляется первым', value: '187' }, + { label: 'Гетерохромия', value: '570' }, + { label: 'Гильдии', value: '411' }, + { label: 'Гипнотизм', value: '755' }, + { label: 'Главный герой — бог', value: '547' }, + { label: 'Главный герой — гуманоид', value: '636' }, + { label: 'Главный герой — наполовину чел', value: '445' }, + { label: 'Главный герой — отец', value: '792' }, + { label: 'Главный герой — раб', value: '780' }, + { label: 'Главный герой — ребенок', value: '488' }, + { label: 'Главный герой — рубака', value: '476' }, + { label: 'Главный герой влюбляется первы', value: '683' }, + { label: 'Главный герой играет роль', value: '472' }, + { label: 'Главный герой носит очки', value: '672' }, + { label: 'Главный герой пацифист', value: '698' }, + { label: 'Гладиаторы', value: '603' }, + { label: 'Глуповатый главный герой', value: '385' }, + { label: 'Гоблины', value: '586' }, + { label: 'Големы', value: '620' }, + { label: 'Гомункул', value: '789' }, + { label: 'Горничные', value: '459' }, + { label: 'Городское фэнтези', value: '105' }, + { label: 'Госпиталь', value: '800' }, + { label: 'Готовка', value: '302' }, + { label: 'Гриндинг', value: '393' }, + { label: 'Дао Компаньон', value: '462' }, + { label: 'Даосизм', value: '760' }, + { label: 'Дварфы', value: '323' }, + { label: 'Двойная личность', value: '642' }, + { label: 'Двойник', value: '601' }, + { label: 'Дворецкий', value: '660' }, + { label: 'Дворяне', value: '168' }, + { label: 'Дворянство/Аристократия', value: '437' }, + { label: 'Девушки-монстры', value: '669' }, + { label: 'Демоническая техника культивац', value: '722' }, + { label: 'Демоны', value: '152' }, + { label: 'Денежный долг', value: '783' }, + { label: 'Депрессия', value: '555' }, + { label: 'Детектив', value: '116' }, + { label: 'Детективы', value: '612' }, + { label: 'Детская литература', value: '123' }, + { label: 'Дискриминация', value: '162' }, + { label: 'Добыча денег в приоритете', value: '396' }, + { label: 'Документальная проза', value: '122' }, + { label: 'Долгая разлука', value: '307' }, + { label: 'Домашние дела', value: '290' }, + { label: 'Домогательство', value: '470' }, + { label: 'Драконы', value: '303' }, + { label: 'Драконьи всадники', value: '607' }, + { label: 'Древние времена', value: '221' }, + { label: 'Древний Китай', value: '374' }, + { label: 'Дружба', value: '217' }, + { label: 'Друзья детства', value: '282' }, + { label: 'Друзья становятся врагами', value: '567' }, + { label: 'Друиды', value: '497' }, + { label: 'Дух лисы', value: '784' }, + { label: 'Духи/призраки', value: '172' }, + { label: 'Духовный советник', value: '435' }, + { label: 'Душевность', value: '633' }, + { label: 'Души', value: '254' }, + { label: 'Европейская атмосфера', value: '521' }, + { label: 'Ёкаи', value: '575' }, + { label: 'Есть аниме-адаптация', value: '154' }, + { label: 'Есть видеоигра по мотивам', value: '552' }, + { label: 'Есть манга', value: '155' }, + { label: 'Есть манхва-адаптация', value: '517' }, + { label: 'Есть маньхуа-адаптация', value: '388' }, + { label: 'Есть сериал-адаптация', value: '492' }, + { label: 'Есть фильм', value: '173' }, + { label: 'Женища-наставник', value: '505' }, + { label: 'Жесткая, двуличная личность', value: '346' }, + { label: 'Жестокие персонажи', value: '503' }, + { label: 'Жестокое обращение с ребенком', value: '654' }, + { label: 'Жестокость', value: '245' }, + { label: 'Животноводство', value: '754' }, + { label: 'Животные черты', value: '528' }, + { label: 'Жизнь в квартире', value: '606' }, + { label: 'Жрицы', value: '615' }, + { label: 'Заботливый главный герой', value: '288' }, + { label: 'Забывчивый главный герой', value: '628' }, + { label: 'Заговоры', value: '289' }, + { label: 'Закалка тела', value: '362' }, + { label: 'Законники', value: '756' }, + { label: 'Замкнутый главный герой', value: '589' }, + { label: 'Запечатанная сила', value: '430' }, + { label: 'Застенчивые персонажи', value: '508' }, + { label: 'Звери', value: '237' }, + { label: 'Звери-компаньоны', value: '301' }, + { label: 'Злой протагонист', value: '243' }, + { label: 'Злые боги', value: '504' }, + { label: 'Злые организации', value: '563' }, + { label: 'Злые религии', value: '734' }, + { label: 'Знаменитости', value: '473' }, + { label: 'Знаменитый главный герой', value: '531' }, + { label: 'Знания современного мира', value: '297' }, + { label: 'Зомби', value: '409' }, + { label: 'Игра на выживание', value: '274' }, + { label: 'Игривый протагонист', value: '717' }, + { label: 'Игровая система рейтинга', value: '391' }, + { label: 'Игровые элементы', value: '266' }, + { label: 'Игрушки (18+)', value: '676' }, + { label: 'Из грязи в князи', value: '416' }, + { label: 'Из женщины в мужчину', value: '709' }, + { label: 'Из мужчины в женщину', value: '715' }, + { label: 'Из полного в худого', value: '767' }, + { label: 'Из слабого в сильного', value: '202' }, + { label: 'Извращенный главный герой', value: '434' }, + { label: 'Изгои', value: '534' }, + { label: 'Изменение расы', value: '664' }, + { label: 'Изменения внешнего вида', value: '300' }, + { label: 'Изменения личности', value: '219' }, + { label: 'Изнасилование', value: '229' }, + { label: 'Изображения жестокости', value: '355' }, + { label: 'ИИ', value: '235' }, + { label: 'Империи', value: '748' }, + { label: 'Инвалидность', value: '684' }, + { label: 'Индустриализация', value: '292' }, + { label: 'Инженер', value: '163' }, + { label: 'Инцест', value: '525' }, + { label: 'Искусственный интеллект', value: '236' }, + { label: 'Исследования', value: '670' }, + { label: 'Историческая проза', value: '119' }, + { label: 'Исторические приключения', value: '120' }, + { label: 'Исторический детектив', value: '117' }, + { label: 'Исторический любовный роман', value: '138' }, + { label: 'Историческое фэнтези', value: '110' }, + { label: 'Каннибализм', value: '456' }, + { label: 'Карточные игры', value: '682' }, + { label: 'Киберпанк', value: '141' }, + { label: 'Киберспорт', value: '686' }, + { label: 'Кланы', value: '796' }, + { label: 'Класс безработного', value: '735' }, + { label: 'Клоны', value: '447' }, + { label: 'Клубы', value: '216' }, + { label: 'Книги', value: '427' }, + { label: 'Книги навыков', value: '400' }, + { label: 'Книжный червь', value: '519' }, + { label: 'Коварство', value: '230' }, + { label: 'Коллеги', value: '704' }, + { label: 'Колледж/Университет', value: '726' }, + { label: 'Кома', value: '776' }, + { label: 'Командная работа', value: '496' }, + { label: 'Комедийный оттенок', value: '582' }, + { label: 'Комплекс неполноценности', value: '550' }, + { label: 'Комплекс семейных отношений', value: '223' }, + { label: 'Конкуренция', value: '746' }, + { label: 'Контроль разума/сознания', value: '357' }, + { label: 'Копейщик', value: '425' }, + { label: 'Королевская битва', value: '797' }, + { label: 'Королевская власть', value: '178' }, + { label: 'Королевства', value: '259' }, + { label: 'Короткий любовный роман', value: '136' }, + { label: 'Коррупция', value: '457' }, + { label: 'Космическая фантастика', value: '146' }, + { label: 'Космические войны', value: '697' }, + { label: 'Красивый герой', value: '226' }, + { label: 'Крафт', value: '377' }, + { label: 'Кризис личности', value: '640' }, + { label: 'Кругосветное путешествие', value: '353' }, + { label: 'Кудере', value: '506' }, + { label: 'Кузены', value: '718' }, + { label: 'Кузнец', value: '518' }, + { label: 'Кукловоды', value: '501' }, + { label: 'Куклы/марионетки', value: '624' }, + { label: 'Культивация', value: '241' }, + { label: 'Куннилингус', value: '667' }, + { label: 'Легенды', value: '500' }, + { label: 'Легкая жизнь', value: '644' }, + { label: 'Ленивый главный герой', value: '621' }, + { label: 'Лидерство', value: '494' }, + { label: 'Лоли', value: '213' }, + { label: 'Лотерея', value: '477' }, + { label: 'Любовное фэнтези', value: '135' }, + { label: 'Любовный треугольник', value: '218' }, + { label: 'Любовь детства', value: '560' }, + { label: 'Любовь с первого взгляда', value: '737' }, + { label: 'Магические надписи', value: '453' }, + { label: 'Магические печати', value: '369' }, + { label: 'Магические технологии', value: '440' }, + { label: 'Магия', value: '165' }, + { label: 'Магия призыва', value: '418' }, + { label: 'Мазохистские персонажи', value: '688' }, + { label: 'Манипулятивные персонажи', value: '249' }, + { label: 'Мания', value: '212' }, + { label: 'Мастер на все руки', value: '466' }, + { label: 'Мастурбация', value: '788' }, + { label: 'Махо-сёдзё', value: '556' }, + { label: 'Медицинские знания', value: '507' }, + { label: 'Медленная романтическая линия', value: '232' }, + { label: 'Медленное развитие на старте', value: '694' }, + { label: 'Межпространственные путешестви', value: '404' }, + { label: 'Менеджмент', value: '294' }, + { label: 'Мертвый главный герой', value: '761' }, + { label: 'Месть', value: '209' }, + { label: 'Метаморфы', value: '335' }, + { label: 'Меч и магия', value: '180' }, + { label: 'Мечник', value: '647' }, + { label: 'Мечты', value: '738' }, + { label: 'Милая история', value: '725' }, + { label: 'Милое дитя', value: '637' }, + { label: 'Милый главный герой', value: '714' }, + { label: 'Мировое дерево', value: '463' }, + { label: 'Мистика', value: '151' }, + { label: 'Мистический ореол вокруг семьи', value: '483' }, + { label: 'Мифические звери', value: '491' }, + { label: 'Мифология', value: '530' }, + { label: 'Младшие братья', value: '785' }, + { label: 'Младшие сестры', value: '527' }, + { label: 'ММОРПГ (ЛитРПГ)', value: '395' }, + { label: 'Множество перемещенных людей', value: '549' }, + { label: 'Множество реальностей', value: '370' }, + { label: 'Множество реинкарнированных лю', value: '330' }, + { label: 'Модели', value: '678' }, + { label: 'Молчаливый персонаж', value: '721' }, + { label: 'Монстры', value: '192' }, + { label: 'Мужская гей-пара', value: '706' }, + { label: 'Мужчина-яндере', value: '267' }, + { label: 'Музыка', value: '635' }, + { label: 'Музыкальные группы', value: '634' }, + { label: 'Мутации', value: '693' }, + { label: 'Мутированные существа', value: '405' }, + { label: 'Навык кражи', value: '662' }, + { label: 'Навязчивая любовь', value: '206' }, + { label: 'Наемники', value: '412' }, + { label: 'Назойливый возлюбленный', value: '561' }, + { label: 'Наивный главный герой', value: '189' }, + { label: 'Наркотики', value: '689' }, + { label: 'Нарциссический главный герой', value: '532' }, + { label: 'Насилие сексуального характера', value: '774' }, + { label: 'Наследование', value: '452' }, + { label: 'Научная фантастика', value: '144' }, + { label: 'Национализм', value: '406' }, + { label: 'Не блещущий внешне главный гер', value: '415' }, + { label: 'Не родные братья и сестры', value: '526' }, + { label: 'Небеса', value: '568' }, + { label: 'Небесное испытание', value: '367' }, + { label: 'Негуманоидный главный герой', value: '659' }, + { label: 'Недоверчивый главный герой', value: '546' }, + { label: 'Недооцененный главный герой', value: '258' }, + { label: 'Недоразумения', value: '309' }, + { label: 'Неизлечимая болезнь', value: '196' }, + { label: 'Некромант', value: '397' }, + { label: 'Нелинейная история', value: '269' }, + { label: 'Ненавистный главный герой', value: '548' }, + { label: 'Ненадежный рассказчик', value: '277' }, + { label: 'Нерезиденты', value: '772' }, + { label: 'Нерешительный главный герой', value: '744' }, + { label: 'Несерьезный главный герой', value: '384' }, + { label: 'Несколько временных линий', value: '576' }, + { label: 'Несколько гг', value: '268' }, + { label: 'Несколько главных героев', value: '537' }, + { label: 'Несколько идентичностей', value: '652' }, + { label: 'Несколько личностей', value: '536' }, + { label: 'Нетораре', value: '732' }, + { label: 'Нетори', value: '514' }, + { label: 'Неудачливый главный герой', value: '618' }, + { label: 'Ниндзя', value: '441' }, + { label: 'Новелла', value: '103' }, + { label: 'Обещание из детства', value: '742' }, + { label: 'Обманщик', value: '485' }, + { label: 'Обмен телами', value: '214' }, + { label: 'Обнаженка', value: '490' }, + { label: 'Обольщение', value: '731' }, + { label: 'Оборотни', value: '661' }, + { label: 'Обратный гарем', value: '351' }, + { label: 'Общество монстров', value: '329' }, + { label: 'Обязательство', value: '602' }, + { label: 'Огнестрельное оружие', value: '291' }, + { label: 'Ограниченное время жизни', value: '194' }, + { label: 'Одержимость', value: '512' }, + { label: 'Одинокий главный герой', value: '394' }, + { label: 'Одиночество', value: '306' }, + { label: 'Одиночное проживание', value: '313' }, + { label: 'Околосмертные переживания', value: '759' }, + { label: 'Оммёдзи', value: '597' }, + { label: 'Омоложение', value: '338' }, + { label: 'Организованная преступность', value: '540' }, + { label: 'Оргия', value: '775' }, + { label: 'Орки', value: '331' }, + { label: 'Освоение навыков', value: '336' }, + { label: 'Основано на аниме', value: '605' }, + { label: 'Основано на видео игре', value: '720' }, + { label: 'Основано на визуальной новелле', value: '793' }, + { label: 'Основано на песне', value: '700' }, + { label: 'Основано на фильме', value: '604' }, + { label: 'Осторожный главный герой', value: '240' }, + { label: 'Отаку', value: '571' }, + { label: 'Открытый космос', value: '264' }, + { label: 'Отношения в сети', value: '727' }, + { label: 'Отношения между людьми и нелюд', value: '311' }, + { label: 'Отношения на расстоянии', value: '791' }, + { label: 'Отношения Сенпай-Коухай', value: '572' }, + { label: 'Отношения ученика и учителя', value: '515' }, + { label: 'Отношения учитель-ученик', value: '429' }, + { label: 'Отношения хозяин-слуга', value: '312' }, + { label: 'Отомэ игра', value: '733' }, + { label: 'Отсутствие здравого смысла', value: '645' }, + { label: 'Отсутствие родителей', value: '625' }, + { label: 'Офисный роман', value: '707' }, + { label: 'Официанты', value: '703' }, + { label: 'Охотники', value: '378' }, + { label: 'Очаровательный главный герой', value: '687' }, + { label: 'Падшее дворянство', value: '655' }, + { label: 'Падшие ангелы', value: '565' }, + { label: 'Пайзури', value: '665' }, + { label: 'Паразиты', value: '591' }, + { label: 'Параллельные миры', value: '207' }, + { label: 'Парк', value: '265' }, + { label: 'Парк развлечений', value: '632' }, + { label: 'Пародия', value: '407' }, + { label: 'Певцы/Певицы', value: '739' }, + { label: 'Первая любовь', value: '729' }, + { label: 'Первоисточник новеллы — манга', value: '611' }, + { label: 'Первый раз', value: '787' }, + { label: 'Перемещение в игровой мир', value: '588' }, + { label: 'Перемещение в иной мир', value: '750' }, + { label: 'Перерождение в ином мире', value: '751' }, + { label: 'Переселение души/трансмиграция', value: '257' }, + { label: 'Персонаж использует щит', value: '666' }, + { label: 'Петля времени', value: '200' }, + { label: 'Пираты', value: '771' }, + { label: 'Писатели', value: '482' }, + { label: 'Питомцы', value: '349' }, + { label: 'Племенное общество', value: '381' }, + { label: 'Повелитель демонов', value: '283' }, + { label: 'Подземелья', value: '322' }, + { label: 'Подростковая проза', value: '114' }, + { label: 'Пожелания', value: '278' }, + { label: 'Познание Дао', value: '443' }, + { label: 'Покинутое дитя', value: '590' }, + { label: 'Полигамия', value: '386' }, + { label: 'Политика', value: '170' }, + { label: 'Политический роман', value: '133' }, + { label: 'Полиция', value: '764' }, + { label: 'Полулюди', value: '420' }, + { label: 'Популярный любовный интерес', value: '342' }, + { label: 'Постапокалипсис', value: '145' }, + { label: 'Постапокалиптика', value: '184' }, + { label: 'Потерянные цивилизации', value: '749' }, + { label: 'Похищения людей', value: '723' }, + { label: 'Поэзия', value: '111' }, + { label: 'Правонарушители', value: '713' }, + { label: 'Прагматичный главный герой', value: '592' }, + { label: 'Преданный любовный интерес', value: '225' }, + { label: 'Предательство', value: '222' }, + { label: 'Предвидение', value: '177' }, + { label: 'Прекрасная героиня', value: '158' }, + { label: 'Преступники', value: '736' }, + { label: 'Преступность', value: '204' }, + { label: 'Призванный герой', value: '262' }, + { label: 'Призраки', value: '176' }, + { label: 'Принуждение к отношениям', value: '643' }, + { label: 'Принцессы', value: '795' }, + { label: 'Притворная пара', value: '641' }, + { label: 'Причудливые персонажи', value: '387' }, + { label: 'Пришельцы/инопланетяне', value: '197' }, + { label: 'Программист', value: '691' }, + { label: 'Проклятия', value: '174' }, + { label: 'Промывание мозгов', value: '578' }, + { label: 'Пропуск времени', value: '256' }, + { label: 'Пророчества', value: '499' }, + { label: 'Проститутки', value: '786' }, + { label: 'Прошлое играет большую роль', value: '320' }, + { label: 'Прыжки между мирами', value: '741' }, + { label: 'Психические силы', value: '359' }, + { label: 'Психопаты', value: '270' }, + { label: 'Публицистика', value: '124' }, + { label: 'Путешествие во времени', value: '201' }, + { label: 'Пытка', value: '280' }, + { label: 'Рабы', value: '778' }, + { label: 'Развитие личности', value: '125' }, + { label: 'Развод', value: '758' }, + { label: 'Разумные предметы', value: '286' }, + { label: 'Расизм', value: '408' }, + { label: 'Рассказ', value: '185' }, + { label: 'Расторжения помолвки', value: '343' }, + { label: 'Расы зооморфов', value: '315' }, + { label: 'Реализм', value: '246' }, + { label: 'Ревность', value: '730' }, + { label: 'Редакторы', value: '705' }, + { label: 'Реинкарнация', value: '372' }, + { label: 'Реинкарнация в монстра', value: '310' }, + { label: 'Реинкарнация в объект', value: '711' }, + { label: 'Религии', value: '616' }, + { label: 'Репортеры', value: '781' }, + { label: 'Ресторан', value: '680' }, + { label: 'Решительный главный герой', value: '242' }, + { label: 'Робкий главный герой', value: '763' }, + { label: 'Родитель одиночка', value: '708' }, + { label: 'Родительский комплекс', value: '587' }, + { label: 'Родословная', value: '239' }, + { label: 'Романтическая эротика', value: '132' }, + { label: 'Романтический подсюжет', value: '271' }, + { label: 'Рост персонажа', value: '215' }, + { label: 'Рыцари', value: '260' }, + { label: 'Садистские персонажи', value: '674' }, + { label: 'Самоотверженный главный герой', value: '747' }, + { label: 'Самурай', value: '677' }, + { label: 'Сборник коротких историй', value: '520' }, + { label: 'Связанные сюжетные линии', value: '535' }, + { label: 'Святые', value: '752' }, + { label: 'Священники', value: '413' }, + { label: 'Сдержанный главный герой', value: '762' }, + { label: 'Секретные организации', value: '417' }, + { label: 'Секреты', value: '626' }, + { label: 'Секс рабы', value: '779' }, + { label: 'Семейный конфликт', value: '757' }, + { label: 'Семь добродетелей', value: '334' }, + { label: 'Семь смертных грехов', value: '253' }, + { label: 'Семья', value: '347' }, + { label: 'Сёнэн-ай подсюжет', value: '690' }, + { label: 'Серийные убийцы', value: '557' }, + { label: 'Сестринский комплекс', value: '558' }, + { label: 'Сила духа', value: '373' }, + { label: 'Сила, требующая платы за польз', value: '579' }, + { label: 'Сильная пара', value: '228' }, + { label: 'Сильный в сильнейшего', value: '442' }, + { label: 'Сильный любовный интерес', value: '273' }, + { label: 'Синдром восьмиклассника', value: '211' }, + { label: 'Сироты', value: '319' }, + { label: 'Система уровней', value: '305' }, + { label: 'Системный администратор', value: '538' }, + { label: 'Сказка', value: '126' }, + { label: 'Скрытие истинной личности', value: '451' }, + { label: 'Скрытие истинных способностей', value: '348' }, + { label: 'Скрытный главный герой', value: '252' }, + { label: 'Скрытые способности', value: '247' }, + { label: 'Скульпторы', value: '448' }, + { label: 'Слабо выраженная романтическая', value: '318' }, + { label: 'Слабый главный герой', value: '298' }, + { label: 'Слепой главный герой', value: '794' }, + { label: 'Слуги', value: '333' }, + { label: 'Слэш', value: '131' }, + { label: 'Смерть', value: '175' }, + { label: 'Смерть близких', value: '444' }, + { label: 'Собственнические персонажи', value: '227' }, + { label: 'Современная проза', value: '150' }, + { label: 'Современность', value: '167' }, + { label: 'Современный любовный роман', value: '137' }, + { label: 'Сожительство', value: '544' }, + { label: 'Создание армии', value: '287' }, + { label: 'Создание артефактов', value: '382' }, + { label: 'Создание клана', value: '513' }, + { label: 'Создание королевства', value: '293' }, + { label: 'Создание навыков', value: '337' }, + { label: 'Создание секты', value: '469' }, + { label: 'Солдаты/военные', value: '193' }, + { label: 'Сон', value: '622' }, + { label: 'Состоятельные персонажи', value: '190' }, + { label: 'Социальная иерархия по силе', value: '255' }, + { label: 'Социальная фантастика', value: '148' }, + { label: 'Социальные изгои', value: '542' }, + { label: 'Спасение мира', value: '638' }, + { label: 'Специальные способности', value: '179' }, + { label: 'Спокойный главный герой', value: '160' }, + { label: 'Справедливый главный герой', value: '719' }, + { label: 'Средневековье', value: '296' }, + { label: 'Ссорящаяся пара', value: '383' }, + { label: 'Сталкеры', value: '710' }, + { label: 'Старение', value: '299' }, + { label: 'Стимпанк', value: '139' }, + { label: 'Стоические персонажи', value: '509' }, + { label: 'Стокгольмский синдром', value: '675' }, + { label: 'Стратег', value: '495' }, + { label: 'Стратегические битвы', value: '272' }, + { label: 'Стратегия', value: '798' }, + { label: 'Стрелки', value: '522' }, + { label: 'Стрельба из лука', value: '461' }, + { label: 'Студенческий совет', value: '551' }, + { label: 'Судьба', value: '364' }, + { label: 'Суккубы', value: '545' }, + { label: 'Супер герои', value: '799' }, + { label: 'Суровая подготовка', value: '356' }, + { label: 'Таинственная болезнь', value: '195' }, + { label: 'Таинственное прошлое', value: '358' }, + { label: 'Тайная личность', value: '399' }, + { label: 'Тайные отношения', value: '768' }, + { label: 'Танцоры', value: '782' }, + { label: 'Телохранители', value: '516' }, + { label: 'Темное фэнтези', value: '106' }, + { label: 'Тентакли', value: '712' }, + { label: 'Террористы', value: '574' }, + { label: 'Технологический разрыв', value: '658' }, + { label: 'Тихие персонажи', value: '600' }, + { label: 'Толстый главный герой', value: '389' }, + { label: 'Торговцы', value: '489' }, + { label: 'Травля/буллинг', value: '210' }, + { label: 'Травник', value: '724' }, + { label: 'Трагическое прошлое', value: '276' }, + { label: 'Трансплантация воспоминаний', value: '449' }, + { label: 'Триллер', value: '134' }, + { label: 'Трудолюбивый главный герой', value: '164' }, + { label: 'Тюрьма', value: '541' }, + { label: 'Убийства', value: '205' }, + { label: 'Убийцы', value: '345' }, + { label: 'Убийцы драконов', value: '646' }, + { label: 'Уверенный главный герой', value: '363' }, + { label: 'Удачливый главный герой', value: '478' }, + { label: 'Ужасы', value: '112' }, + { label: 'Укротитель монстров', value: '422' }, + { label: 'Умения из прошлой жизни', value: '371' }, + { label: 'Умная пара', value: '554' }, + { label: 'Умный главный герой', value: '161' }, + { label: 'Уникальная техника Культивации', value: '350' }, + { label: 'Уникальное оружие', value: '426' }, + { label: 'Управление бизнесом', value: '403' }, + { label: 'Управление временем', value: '279' }, + { label: 'Управление кровью', value: '753' }, + { label: 'Упрямый главный герой', value: '695' }, + { label: 'Уродливый главный герой', value: '766' }, + { label: 'Ускоренный рост', value: '390' }, + { label: 'Усыновленные дети', value: '543' }, + { label: 'Усыновленный главный герой', value: '486' }, + { label: 'Уход за детьми', value: '474' }, + { label: 'Учителя', value: '431' }, + { label: 'Фамильяры', value: '596' }, + { label: 'Фанатизм', value: '650' }, + { label: 'Фантастические существа', value: '410' }, + { label: 'Фантастический детектив', value: '115' }, + { label: 'Фанфик', value: '113' }, + { label: 'Фанфикшн', value: '464' }, + { label: 'Фармацевт', value: '728' }, + { label: 'Фарминг', value: '458' }, + { label: 'Феи', value: '316' }, + { label: 'Фелляция', value: '631' }, + { label: 'Фемслэш', value: '127' }, + { label: 'Фениксы', value: '454' }, + { label: 'Фетиш груди', value: '559' }, + { label: 'Философия', value: '208' }, + { label: 'Фильмы', value: '479' }, + { label: 'Флэшбэки', value: '585' }, + { label: 'Фобии', value: '220' }, + { label: 'Фольклор', value: '529' }, + { label: 'Футанари', value: '619' }, + { label: 'Футуристический сеттинг', value: '460' }, + { label: 'Фэнтези мир', value: '244' }, + { label: 'Хакеры', value: '475' }, + { label: 'Харизматический герой', value: '467' }, + { label: 'Хикикомори/Затворники', value: '524' }, + { label: 'Хитроумный главный герой', value: '224' }, + { label: 'Хозяин подземелий', value: '608' }, + { label: 'Холодный главный герой', value: '354' }, + { label: 'Хорошие отношения с семьей', value: '566' }, + { label: 'Хранители могил', value: '191' }, + { label: 'Целители', value: '465' }, + { label: 'Цзянши', value: '740' }, + { label: 'Цундэрэ', value: '510' }, + { label: 'Чаты', value: '629' }, + { label: 'Человеческое оружие', value: '580' }, + { label: 'Честный главный герой', value: '341' }, + { label: 'Читы', value: '339' }, + { label: 'Шантаж', value: '584' }, + { label: 'Шеф-повар', value: '340' }, + { label: 'Шикигами', value: '598' }, + { label: 'Школа только для девочек', value: '668' }, + { label: 'Шота', value: '573' }, + { label: 'Шоу-бизнес', value: '481' }, + { label: 'Шпионский детектив', value: '118' }, + { label: 'Шпионы', value: '614' }, + { label: 'Эволюция', value: '304' }, + { label: 'Эгоистичный главный герой', value: '594' }, + { label: 'Эйдетическая память', value: '468' }, + { label: 'Экзорсизм', value: '564' }, + { label: 'Экономика', value: '553' }, + { label: 'Эксгибиционизм', value: '673' }, + { label: 'Эксперименты с людьми', value: '248' }, + { label: 'Элементальная магия', value: '471' }, + { label: 'Эльфы', value: '284' }, + { label: 'Эмоционально слабый гг', value: '770' }, + { label: 'Эпизодический', value: '649' }, + { label: 'Эпическое фэнтези', value: '109' }, + { label: 'Эротическая фантастика', value: '129' }, + { label: 'Эротический фанфик', value: '128' }, + { label: 'Эротическое фэнтези', value: '130' }, + { label: 'Юмористическая фантастика', value: '143' }, + { label: 'Юмористическое фэнтези', value: '108' }, + { label: 'Юный любовный интерес', value: '511' }, + { label: 'Яды', value: '484' }, + { label: 'Языкастые персонажи', value: '480' }, + { label: 'Языковой барьер', value: '182' }, + { label: 'Яндере', value: '314' }, + { label: 'Японские силы самообороны', value: '610' }, + { label: 'Ярко выраженная романтика', value: '365' }, + { label: 'R-15 Японское возрастное огр.', value: '332' }, + { label: 'R-18', value: '424' }, + ], + type: FilterTypes.ExcludableCheckboxGroup, + }, + types: { + label: 'Типы:', + value: [], + options: [ + { label: 'Авторское', value: '1' }, + { label: 'Другое', value: '7' }, + { label: 'Запад', value: '5' }, + { label: 'Китай', value: '4' }, + { label: 'Корея', value: '3' }, + { label: 'Фанфики', value: '6' }, + { label: 'Япония', value: '2' }, + ], + type: FilterTypes.CheckboxGroup, + }, + status: { + label: 'Статус проекта:', + value: [], + options: [ + { label: 'Анонс', value: '4' }, + { label: 'Закончен', value: '0' }, + { label: 'Заморожен', value: '2' }, + { label: 'Лицензировано', value: '5' }, + { label: 'Нет переводчика', value: '3' }, + { label: 'Продолжается', value: '1' }, + ], + type: FilterTypes.CheckboxGroup, + }, + age_limit: { + label: 'Возрастной рейтинг:', + value: [], + options: [ + { label: '16+', value: '1' }, + { label: '18+', value: '2' }, + { label: 'Для всех', value: '0' }, + ], + type: FilterTypes.CheckboxGroup, + }, + } satisfies Filters; +} + +export default new ReN(); + +type responseNovels = { + id: number; + main_name: string; + secondary_name: string; + type: ItemEntity; + status: ItemEntity; + translate_status: ItemEntity; + dir: string; + issue_year: number; + avg_rating: string; + total_votes: number; + total_views: number; + count_bookmarks: number; + is_licensed: boolean; + is_legal: boolean; + cover: Img; + count_chapters: number; + is_yaoi: boolean; + is_erotic: boolean; + bookmark_type: null; + is_forbidden: boolean; +}; + +type Img = { + high?: string; + mid?: string; + low: string; +}; + +type responseNovel = { + id: number; + cover: Img; + secondary_name: string; + main_name: string; + another_name: string; + dir: string; + description: string; + issue_year: number; + avg_rating: string; + admin_rating: string; + count_rating: number; + age_limit: number; + status: ItemEntity; + count_bookmarks: number; + total_votes: number; + total_views: number; + type: ItemEntity; + genres?: ItemEntity[] | null; + categories?: ItemEntity[] | null; + publishers?: PublishersEntity[] | null; + bookmark_type?: null; + branches?: BranchesEntity[] | null; + count_chapters: number; + first_chapter: FirstChapter; + continue_reading?: null; + is_licensed: boolean; + newlate_id?: null; + newlate_title?: null; + related?: null; + uploaded: number; + can_post_comments: boolean; + adaptation: Adaptation | null; +}; + +type Adaptation = { + main_name: string; + secondary_name: string; + dir: string; + img: string; +}; + +type ItemEntity = { + id: number; + name: string; +}; +type PublishersEntity = { + id: number; + name: string; + img: string; + dir: string; + tagline: string; + type: string; +}; +type BranchesEntity = { + id: number; + img: string; + publishers?: PublishersEntity[] | null; + subscribed: boolean; + total_votes: number; + count_chapters: number; +}; +type FirstChapter = { + id: number; + tome: number; + chapter: string; +}; + +type responseСhapters = { + id: number; + rated?: null; + viewed?: null; + is_bought?: boolean | null; + publishers?: PublishersEntity[] | null; + tome: number; + index: number; + chapter: string; + name: string; + price?: null | number; + score: number; + pub_date?: null; + upload_date: string; + is_paid: boolean; +}; + +type responseСhapter = { + id: number; + chapter: string; + name: string; + content: string; + score: number; + upload_date: string; + is_paid: boolean; + title_id: number; + volume_id: number; + branch_id: number; + price?: null; + pub_date?: null; + index: number; + publishers?: PublishersEntity[] | null; + tome: number; +}; diff --git a/plugins/russian/ruvers.broken.ts b/plugins/russian/ruvers.broken.ts new file mode 100644 index 000000000..990fc3fc5 --- /dev/null +++ b/plugins/russian/ruvers.broken.ts @@ -0,0 +1,185 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { defaultCover } from '@libs/defaultCover'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; + +class RV implements Plugin.PluginBase { + id = 'RV'; + name = 'Ruvers'; + site = 'https://ruvers.ru/'; + version = '1.0.0'; + icon = 'src/ru/ruvers/icon.png'; + + async fetchNovels( + page: number, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + searchTerm?: string, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + 'api/books?page=' + page; + url += '&sort=' + (filters?.sort?.value || '-rating'); + + if (searchTerm) url += '&search=' + searchTerm; + + const { data }: { data: book[] } = await fetchApi(url).then( + (res: Response) => res.json(), + ); + const novels: Plugin.NovelItem[] = []; + + data.forEach(novel => + novels.push({ + name: novel.name, + cover: novel.images[0] ? this.site + novel.images[0] : defaultCover, + path: novel.slug, + }), + ); + + return novels; + } + + async popularNovels( + page: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + return this.fetchNovels(page, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, undefined, searchTerm); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then((res: Response) => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('div.name > h1').text().trim(), + cover: loadedCheerio('.slider_prods_single > img').attr('src'), + summary: loadedCheerio('.book_description').text().trim(), + genres: loadedCheerio('.genres > a') + .map((index, element) => loadedCheerio(element).text()?.trim()) + .get() + .join(','), + status: loadedCheerio('.status_row > div:nth-child(1) > a') + .text() + .includes('В работе') + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + const bookId = loadedCheerio('comments-list').attr('commentable-id'); + + const chaptersJSON: { data: chapters[] } = await fetchApi( + this.site + 'api/books/' + bookId + '/chapters/all', + ).then((res: Response) => res.json()); + + if (chaptersJSON.data.length) { + const chapters: Plugin.ChapterItem[] = []; + chaptersJSON.data.forEach((chapter, chapterIndex) => { + if ( + chapter.is_published && + (chapter.is_free || chapter.purchased_by_user) + ) { + chapters.push({ + name: 'Глава ' + chapter.number + ' ' + (chapter.name || ''), + path: novelPath + '/' + chapter.id, + releaseTime: dayjs(chapter.created_at).format('LLL'), + chapterNumber: chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + } + + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then((res: Response) => + res.text(), + ); + const encrypted = body.match( + /(mobile-books|books)-chapters-text-component.*:text='"(.*?)"'/s, + )?.[2]; + if (!encrypted) throw new Error('No chapter found'); + + return unicodeToUtf8(encrypted); + } + + filters = { + sort: { + label: 'Сортировка', + value: '-rating', + options: [ + { label: 'По названию', value: 'name' }, + { label: 'По дате добавления', value: '-created_at' }, + { label: 'По рейтингу', value: '-rating' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new RV(); + +type book = { + id: number; + name: string; + original_name: string; + description: string; + publishing_house?: string | null; + country: string; + original_author: string; + images: string[]; + price?: number | null; + is_verified: boolean; + rating: number; + type: string; + release_year: number; + tags?: string[] | null; + isbn?: null; + alternative_title?: string | null; + count_chapters: number; + chapter_open_days?: number | null; + age_category_id: number; + status_id: number; + writer_id: number; + created_at: string; + updated_at: string; + slug: string; + chapters_count: number; +}; + +type chapters = { + id: number; + name: string; + number: string; + price?: string | null; + open_by_timer: boolean; + order: number; + opened_at?: string | null; + rating?: null; + book_id: number; + deleted_at?: string | null; + created_at: string; + updated_at: string; + is_published: boolean; + first_published_at: string; + full_name: string; + is_free: boolean; + is_new: boolean; + purchased_by_user: boolean; + book_slug: string; +}; + +function unicodeToUtf8(unicode: string) { + return unicode.replace(/\\u([\d\w]{4})/gi, (match, hex) => + String.fromCharCode(parseInt(hex, 16)), + ); +} diff --git a/plugins/russian/topliba.ts b/plugins/russian/topliba.ts new file mode 100644 index 000000000..917123b4d --- /dev/null +++ b/plugins/russian/topliba.ts @@ -0,0 +1,166 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { load as parseHTML } from 'cheerio'; + +class TopLiba implements Plugin.PluginBase { + id = 'TopLiba'; + name = 'ТопЛиба'; + site = 'https://topliba.com'; + version = '1.0.1'; + icon = 'src/ru/topliba/icon.png'; + _token = ''; + + async fetchNovels( + page: number, + showLatestNovels?: boolean, + filters?: Plugin.PopularNovelsOptions<typeof this.filters>['filters'], + searchTerm?: string, + ): Promise<Plugin.NovelItem[]> { + const data = new URLSearchParams({ + order_field: showLatestNovels ? 'date' : filters?.sort?.value || 'rating', + p: page.toString(), + }); + + if (searchTerm) data.append('q', searchTerm); + const body = await fetchApi(this.site + '/?' + data.toString()).then(res => + res.text(), + ); + const novels: Plugin.NovelItem[] = []; + + this._token = body.match( + /<meta name="_token" content="(.*?)"/, + )?.[1] as string; + + const elements = body.match(/<img class="cover" data-original=".*>/g) || []; + elements.forEach(element => { + const [, path, name] = + element.match(/data-original=".*covers\/(.*?)_.*title="(.*?)"/) || []; + if (path && name) { + novels.push({ + name, + cover: this.site + '/covers/' + path + '.jpg', + path, + }); + } + }); + + return novels; + } + + async popularNovels( + page: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ) { + return this.fetchNovels(page, showLatestNovels, filters); + } + + async searchNovels(searchTerm: string, page: number) { + return this.fetchNovels(page, false, undefined, searchTerm); + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('div > h1').text().trim(), + cover: this.site + '/covers/' + novelPath + '.jpg', + summary: loadedCheerio('.description').text().trim(), + author: loadedCheerio('.book-author > a').text().trim(), + genres: loadedCheerio('.book-genres > div > a') + .map((index, element) => loadedCheerio(element).text()) + .get() + .join(','), + }; + + const chaptersHTML = await fetchApi(this.resolveUrl(novelPath)).then(res => + res.text(), + ); + + this._token = + chaptersHTML.match(/<meta name="_token" content="(.*?)"/)?.[1] || + body.match(/<meta name="_token" content="(.*?)"/)?.[1] || + this._token; + + const chapters: Plugin.ChapterItem[] = []; + const elements = + chaptersHTML.match( + /<li class="padding-\d+" data-capter="\d+">([\s\S]*?)</g, + ) || []; + elements.forEach((chapter, chapterIndex) => { + const [, padding, capter, name] = + chapter.match( + /class="padding-(\d+)" data-capter="(\d+)">([\s\S]*?)</, + ) || []; + + if (padding && capter && name) { + const id = + padding === '0' ? capter : padding + '-' + (parseInt(capter, 10) - 1); + + chapters.push({ + name: name.trim(), + path: novelPath + '?' + id, + releaseTime: null, + chapterNumber: chapterIndex + 1, + }); + } + }); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [bookID, chapterID] = chapterPath.split('?'); + if (!this._token) { + const chaptersHTML = await fetchApi(this.resolveUrl(bookID)).then(res => + res.text(), + ); + this._token = chaptersHTML.match( + /<meta name="_token" content="(.*?)"/, + )?.[1] as string; + } + + const chapterText = await fetchApi(this.resolveUrl(bookID) + '/chapter', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + Referer: this.resolveUrl(bookID), + Origin: this.site, + }, + method: 'POST', + body: new URLSearchParams({ + chapter: chapterID, + _token: this._token, + }).toString(), + }).then(res => res.text()); + + return chapterText; + } + + resolveUrl = (path: string, isNovel?: boolean) => + this.site + (isNovel ? '/books/' + path : '/reader/' + path.split('?')[0]); + + filters = { + sort: { + label: 'Сортировка:', + value: 'rating', + options: [ + { label: 'По рейтингу', value: 'rating' }, + { label: 'По популярности', value: 'num_downloads' }, + { label: 'По году выхода', value: 'year' }, + { label: 'По дате добавления', value: 'date' }, + { label: 'По названию', value: 'title' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new TopLiba(); diff --git a/plugins/russian/zelluloza.ts b/plugins/russian/zelluloza.ts new file mode 100644 index 000000000..ac74d0521 --- /dev/null +++ b/plugins/russian/zelluloza.ts @@ -0,0 +1,396 @@ +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { load as parseHTML } from 'cheerio'; +import dayjs from 'dayjs'; + +class Zelluloza implements Plugin.PluginBase { + id = 'zelluloza'; + name = 'Целлюлоза'; + site = 'https://zelluloza.ru'; + version = '1.0.3'; + icon = 'src/ru/zelluloza/icon.png'; + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const sort = showLatestNovels ? '0' : filters?.sort?.value || '3'; + + const genres = filters?.genres?.value || '0'; + + const body = await fetchApi(this.site + '/ajaxcall/', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/search/done/#result', + Origin: this.site, + }, + method: 'POST', + body: new URLSearchParams({ + op: 'morebooks', + par1: '', + par2: `206:0:${genres}:0.${sort}.0.0.0.0.0.0.0.0.0.0.0..0..:${pageNo}`, + par4: '', + }).toString(), + }).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div[style="display: flex;"]').each((index, element) => { + novels.push({ + name: loadedCheerio(element).find('a[class="txt"]').attr('title') || '', + cover: + this.site + + loadedCheerio(element).find('img[class="shadow"]').attr('src'), + path: ( + loadedCheerio(element).find('a[class="txt"]').attr('href') || '' + ).replace(/\D/g, ''), + }); + }); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.resolveUrl(novelPath)).then(res => + res.text(), + ); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h2[class="bookname"]').text().trim(), + cover: this.site + loadedCheerio('img[class="shadow"]').attr('src'), + genres: loadedCheerio('.gnres span[itemprop="genre"]') + .map((index, element) => loadedCheerio(element).text()) + .get() + .join(','), + summary: + loadedCheerio('#bann_full').text() || + loadedCheerio('#bann_short').text(), + author: loadedCheerio('.author_link').text(), + status: loadedCheerio('.tech_decription').text().includes('Пишется') + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul[class="g0"] div[class="w800_m"]').each( + (chapterIndex, element) => { + const isFree = loadedCheerio(element).find( + 'div[class="chaptfree"]', + ).length; + if (isFree) { + const chapter = loadedCheerio(element).find('a[class="chptitle"]'); + const releaseDate = loadedCheerio(element) + .find('div[class="stat"]') + .text(); + chapters.push({ + name: chapter.text().trim(), + path: (chapter.attr('href') || '').split('/').slice(2, 4).join('/'), + releaseTime: this.parseDate(releaseDate), + chapterNumber: chapterIndex + 1, + }); + } + }, + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [bookID, chapterID] = chapterPath.split('/'); + const body = await fetchApi(this.site + '/ajaxcall/', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.resolveUrl(chapterPath), + Origin: this.site, + }, + method: 'POST', + body: new URLSearchParams({ + op: 'getbook', + par1: bookID, + par2: chapterID, + }).toString(), + }).then(res => res.text()); + + const encrypted = body.split('<END>')[0].split('\n'); + const decrypted = encrypted + .map(str => decrypt(str)) + .join('') + .replace(/\r/g, '') + .trim(); + + const chapterText = decrypted //cosmetic filters + .replace(/\[\*]([\s\S]*?)\[\/]/g, '<b>$1</b>') + .replace(/\[_]([\s\S]*?)\[\/]/g, '<u>$1</u>') + .replace(/\[-]([\s\S]*?)\[\/]/g, '<s>$1</s>') + .replace(/\[~]([\s\S]*?)\[\/]/g, '<i>$1</i>'); /* + .replace(/\[!]([\s\S]*?)\[\/]/g, '<span style="font-family: Times New Roman">$1</span>') + .replace(/\[ctr]([\s\S]*?)\[\/]/g, "<center>$1</center>") + .replace(/\[rht]([\s\S]*?)\[\/]/g, '<span style="text-align: right">$1</span>') + .replace(/\[grn]([\s\S]*?)\[\/]/g, '<span style="color: #019208">$1</span>') + .replace(/\[red]([\s\S]*?)\[\/]/g, '<span style="color: #E84040">$1</span>') + .replace(/\[blu]([\s\S]*?)\[\/]/g, '<span style="color: #354FA3">$1</span>') + .replace(/\[gry]([\s\S]*?)\[\/]/g, '<span style="color: #909090">$1</span>') + .replace(/\[geo]([\s\S]*?)\[\/]/g, '<span style="font-family: Georgia">$1</span>') + .replace(/\[gld]([\s\S]*?)\[\/]/g, '<span style="color: #988c07">$1</span>');*/ + + return chapterText; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const body = await fetchApi(this.site + '/ajaxcall/', { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Referer: this.site + '/search/done/#result', + Origin: this.site, + }, + method: 'POST', + body: new URLSearchParams({ + op: 'morebooks', + par1: searchTerm || '', + par2: '206:0:0:0.0.0.0.0.0.0.10.0.0.0.0.0..0..:' + pageNo, + par4: '', + }).toString(), + }).then(res => res.text()); + const loadedCheerio = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div[style="display: flex;"]').each((index, element) => { + novels.push({ + name: loadedCheerio(element).find('a[class="txt"]').attr('title') || '', + cover: + this.site + + loadedCheerio(element).find('img[class="shadow"]').attr('src'), + path: ( + loadedCheerio(element).find('a[class="txt"]').attr('href') || '' + ).replace(/\D/g, ''), + }); + }); + + return novels; + } + + parseDate = (dateString: string | undefined = '') => { + const [, day, month, year, hour, minute] = + dateString.match(/(\d{2})\.(\d{2})\.(\d{4})г (\d{2}):(\d{2})/) || []; + + if (day && month && year && hour && minute) { + return dayjs( + year + '-' + month + '-' + day + ' ' + hour + ':' + minute, + ).format('LLL'); + } + + return null; + }; + + resolveUrl = (path: string) => this.site + '/books/' + path; + + filters = { + sort: { + label: 'Сортировка', + value: '3', + options: [ + { label: 'По рейтингу', value: '3' }, + { label: 'По изменению', value: '0' }, + { label: 'По длительности чтения', value: '1' }, + { label: 'По количеству читателей', value: '2' }, + { label: 'По популярности', value: '4' }, + { label: 'Самые продаваемые', value: '5' }, + ], + type: FilterTypes.Picker, + }, + genres: { + label: 'Жанры', + value: '', + options: [ + { label: 'Все', value: '' }, + { label: '16+', value: '204' }, + { label: '18+', value: '100' }, + { label: 'Авантюра', value: '347' }, + { label: 'Альтернативная история', value: '55' }, + { label: 'Ангст', value: '463' }, + { label: 'Аниме', value: '405' }, + { label: 'Антиутопия', value: '21' }, + { label: 'Апокалипсис', value: '368' }, + { label: 'Аудиокнига', value: '391' }, + { label: 'Байки', value: '435' }, + { label: 'Бизнес-книги', value: '522' }, + { label: 'Биография', value: '52' }, + { label: 'Биопанк', value: '379' }, + { label: 'Боевая фантастика', value: '512' }, + { label: 'Боевик', value: '61' }, + { label: 'Боевые искусства', value: '142' }, + { label: 'Бояръ-аниме', value: '433' }, + { label: 'Вбоквелл', value: '372' }, + { label: 'Вестерн', value: '96' }, + { label: 'Война миров', value: '137' }, + { label: 'Восточное фэнтези', value: '505' }, + { label: 'Городское фентези', value: '63' }, + { label: 'Городское фэнтези', value: '520' }, + { label: 'Готика', value: '83' }, + { label: 'Детективы', value: '4' }, + { label: 'Детские книги', value: '8' }, + { label: 'Документальные книги', value: '351' }, + { label: 'Дом и дача', value: '534' }, + { label: 'Дорожные истории', value: '41' }, + { label: 'Драма', value: '47' }, + { label: 'Древняя литература', value: '529' }, + { label: 'Женский психологический роман', value: '186' }, + { label: 'Женский роман', value: '141' }, + { label: 'Зарубежная классика', value: '524' }, + { label: 'Здоровье и красота', value: '533' }, + { label: 'Изучение языков', value: '537' }, + { label: 'Иронический детектив', value: '85' }, + { label: 'Исследования', value: '156' }, + { label: 'Историческая литература', value: '3' }, + { label: 'Исторический любовный роман', value: '518' }, + { label: 'Историческое фэнтези', value: '90' }, + { label: 'Киберпанк', value: '7' }, + { label: 'Классическая литература', value: '9' }, + { label: 'Книга-игра', value: '371' }, + { label: 'Книги для родителей', value: '535' }, + { label: 'Комедия', value: '80' }, + { label: 'Конкурсные сборники', value: '349' }, + { label: 'Короткий любовный роман', value: '517' }, + { label: 'Космическая фантастика', value: '513' }, + { label: 'Космоопера', value: '58' }, + { label: 'Криминальная проза', value: '35' }, + { label: 'Криптоистория', value: '400' }, + { label: 'Ксенофантастика', value: '102' }, + { label: 'Культура и искусство', value: '536' }, + { label: 'Легенды', value: '161' }, + { label: 'Лирика', value: '49' }, + { label: 'ЛитРПГ', value: '62' }, + { label: 'Любовная фантастика', value: '516' }, + { label: 'Любовное фэнтези', value: '515' }, + { label: 'Любовный роман', value: '46' }, + { label: 'Магический реализм', value: '42' }, + { label: 'Матриархат', value: '385' }, + { label: 'Медицина', value: '57' }, + { label: 'Мелодрама', value: '45' }, + { label: 'Мемуары', value: '532' }, + { label: 'Метафизика', value: '430' }, + { label: 'Милитари', value: '126' }, + { label: 'Мистика', value: '22' }, + { label: 'Морские приключения', value: '211' }, + { label: 'Мотивация', value: '135' }, + { label: 'Научная фантастика', value: '122' }, + { label: 'Научно-популярная', value: '43' }, + { label: 'Ненаучная фантастика', value: '387' }, + { label: 'Новелла', value: '498' }, + { label: 'Нон-фикшн', value: '507' }, + { label: 'Нуар', value: '121' }, + { label: 'О войне', value: '136' }, + { label: 'Оккультизм', value: '424' }, + { label: 'Параллельные миры', value: '438' }, + { label: 'Пародия', value: '108' }, + { label: 'Подростковая фантастика', value: '27' }, + { label: 'Подростковый роман', value: '388' }, + { label: 'Подростковый ужастик', value: '110' }, + { label: 'Постапокалипсис', value: '82' }, + { label: 'Постапокалиптика', value: '67' }, + { label: 'Поэзия', value: '44' }, + { label: 'Превращение', value: '170' }, + { label: 'Приключение', value: '497' }, + { label: 'Приключения', value: '5' }, + { label: 'Природа и животные', value: '538' }, + { label: 'Проза', value: '95' }, + { label: 'Производственный роман', value: '148' }, + { label: 'Психология', value: '31' }, + { label: 'Путешествия', value: '383' }, + { label: 'Рассказы', value: '101' }, + { label: 'Реализм', value: '422' }, + { label: 'РеалРПГ', value: '442' }, + { label: 'Религия', value: '188' }, + { label: 'Робинзонада', value: '434' }, + { label: 'Романтика', value: '130' }, + { label: 'Романтическая фантастика', value: '501' }, + { label: 'Русская классика', value: '523' }, + { label: 'Рыцарский роман', value: '112' }, + { label: 'Символизм', value: '439' }, + { label: 'Сказки', value: '93' }, + { label: 'Славянское фэнтези', value: '508' }, + { label: 'Сновидения', value: '428' }, + { label: 'Современный любовный роман', value: '519' }, + { label: 'Социально-психологическая фантастика', value: '98' }, + { label: 'Спорт', value: '147' }, + { label: 'Справочники', value: '528' }, + { label: 'Стимпанк', value: '56' }, + { label: 'Субкультура', value: '209' }, + { label: 'Сюрреализм', value: '26' }, + { label: 'Тёмное фэнтези', value: '437' }, + { label: 'Техномагия', value: '159' }, + { label: 'Триллер', value: '6' }, + { label: 'Трэш', value: '162' }, + { label: 'Ужасы', value: '531' }, + { label: 'Утопия', value: '191' }, + { label: 'Учебник', value: '411' }, + { label: 'Фантастика', value: '1' }, + { label: 'Фантастический боевик', value: '38' }, + { label: 'Фанфик', value: '111' }, + { label: 'Философия', value: '461' }, + { label: 'Фурри', value: '394' }, + { label: 'Фэнтези', value: '2' }, + { label: 'Хентай', value: '441' }, + { label: 'Хобби и досуг', value: '530' }, + { label: 'Хоррор', value: '86' }, + { label: 'Чёрный юмор', value: '73' }, + { label: 'Эзотерика', value: '30' }, + { label: 'Экшн', value: '94' }, + { label: 'Эпическое фэнтези', value: '499' }, + { label: 'Эротика', value: '19' }, + { label: 'Эссе', value: '353' }, + { label: 'ЭТТИ', value: '502' }, + { label: 'Юмористическая литература', value: '20' }, + { label: 'Юмористическая фантастика', value: '514' }, + { label: 'Юмористическое фэнтези', value: '521' }, + { label: 'EVE - Миры Содружества', value: '99' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +const alphabet: Record<string, string> = { + '~': '0', + 'H': '1', + '^': '2', + '@': '3', + 'f': '4', + '0': '5', + '5': '6', + 'n': '7', + 'r': '8', + '=': '9', + 'W': 'a', + 'L': 'b', + '7': 'c', + ' ': 'd', + 'u': 'e', + 'c': 'f', +}; + +function decrypt(encrypt: string) { + if (!encrypt) return ''; + const hexArray = []; + + for (let j = 0; j < encrypt.length; j += 2) { + const firstChar = encrypt.substring(j, j + 1); + const secondChar = encrypt.substring(j + 1, j + 2); + hexArray.push(alphabet[firstChar] + alphabet[secondChar]); + } + + return '<p>' + decodeURIComponent('%' + hexArray.join('%')) + '</p>'; +} + +export default new Zelluloza(); diff --git a/plugins/spanish/NOVA.ts b/plugins/spanish/NOVA.ts new file mode 100644 index 000000000..91f594cc6 --- /dev/null +++ b/plugins/spanish/NOVA.ts @@ -0,0 +1,290 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Element } from 'domhandler'; +import * as cheerio from 'cheerio'; + +class NovaPlugin implements Plugin.PluginBase { + id = 'nova'; + name = 'NOVA'; + icon = 'src/es/nova/icon.png'; + site = 'https://novelasligeras.net'; + version = '1.1.1'; + + // Regex para parsear títulos de capítulos + private readonly CHAPTER_REGEX = /(Parte \d+) . (.+?): (.+)/; + + // Helper para bypass de imágenes de Cloudflare + private async bypassCloudflareImages( + $: cheerio.CheerioAPI, + $content: cheerio.Cheerio<Element>, + ): Promise<string> { + $content.find('img').each((i, img) => { + const $img = $(img); + const src = + $img.attr('src') || $img.attr('data-src') || $img.attr('data-cfsrc'); + + if (src) { + // Si la imagen tiene atributos de Cloudflare, usar la URL directa + $img.attr('src', src); + $img.removeAttr('data-src'); + $img.removeAttr('data-cfsrc'); + } + }); + + return $content.html() || ''; + } + + // Helper para convertir HTML a texto limpio (si es necesario) + private htmlToText(html: string | null | undefined): string { + if (!html) return ''; + const $ = cheerio.load(html); + $('script, style').remove(); + return $.text().trim(); + } + + // Método para obtener novelas populares + async popularNovels( + pageNo: number, + // options: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + // Para la primera página, usar la búsqueda AJAX + if (pageNo === 1) { + return this.searchNovels('', 1); + } + + // Para páginas siguientes, usar la paginación normal + const url = `${this.site}/index.php/page/${pageNo}/?post_type=product&orderby=popularity`; + const body = await fetchApi(url).then(res => res.text()); + const $ = cheerio.load(body); + + const novels: Plugin.NovelItem[] = []; + + $('.dt-css-grid div.wf-cell').each((i, element) => { + const $el = $(element); + const $img = $el.find('img'); + const $link = $el.find('h4.entry-title a'); + + const path = $link.attr('href')?.replace(this.site, '') || ''; + const name = $link.text().trim(); + const cover = + $img.attr('data-src') || + $img.attr('data-cfsrc') || + $img.attr('src') || + ''; + + if (name && path) { + novels.push({ name, path, cover }); + } + }); + + return novels; + } + + // Método para buscar novelas + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const novels: Plugin.NovelItem[] = []; + + if (pageNo > 1) { + // Búsqueda paginada normal + const encodedTerm = encodeURIComponent(searchTerm); + const url = `${this.site}/index.php/page/${pageNo}/?s=${encodedTerm}&post_type=product&title=1&excerpt=1&content=0&categories=1&attributes=1&tags=1&sku=0&orderby=popularity&ixwps=1`; + + const body = await fetchApi(url).then(res => res.text()); + const $ = cheerio.load(body); + + $('.dt-css-grid div.wf-cell').each((i, element) => { + const $el = $(element); + const $img = $el.find('img'); + const $link = $el.find('h4.entry-title a'); + + const path = $link.attr('href')?.replace(this.site, '') || ''; + const name = $link.text().trim(); + const cover = + $img.attr('data-src') || + $img.attr('data-cfsrc') || + $img.attr('src') || + ''; + + if (name && path) { + novels.push({ name, path, cover }); + } + }); + } else { + // Primera página: usar búsqueda AJAX + const url = `${this.site}/wp-admin/admin-ajax.php?tags=1&sku=&limit=30&category_results=&order=DESC&category_limit=5&order_by=title&product_thumbnails=1&title=1&excerpt=1&content=&categories=1&attributes=1`; + + const formData = new FormData(); + formData.append('action', 'product_search'); + formData.append('product-search', '1'); + formData.append('product-query', searchTerm); + + const response = await fetchApi(url, { + method: 'POST', + body: formData, + }); + + const data = await response.json(); + + if (Array.isArray(data)) { + data.forEach(novel => { + const path = novel.url?.replace(this.site, '') || ''; + const name = novel.title || ''; + const cover = novel.thumbnail || ''; + + if (name && path) { + novels.push({ name, path, cover }); + } + }); + } + } + + return novels; + } + + // Método para obtener detalles de una novela + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = `${this.site}${novelPath}`; + const body = await fetchApi(url).then(res => res.text()); + const $ = cheerio.load(body); + + // Extraer información básica + const name = $('h1').first().text().trim(); + const $coverImg = $('.woocommerce-product-gallery').find('img').first(); + const cover = + $coverImg.attr('src') || + $coverImg.attr('data-cfsrc') || + $coverImg.attr('data-src') || + ''; + + // Extraer autor, artista + const author = + $('.woocommerce-product-attributes-item--attribute_pa_escritor td') + .text() + .trim() || 'Desconocido'; + const artist = + $('.woocommerce-product-attributes-item--attribute_pa_ilustrador td') + .text() + .trim() || ''; + + // Extraer resumen + const summaryHtml = $( + '.woocommerce-product-details__short-description', + ).html(); + const summary = this.htmlToText(summaryHtml); + + // Determinar estado + const statusText = $( + '.woocommerce-product-attributes-item--attribute_pa_estado td', + ) + .text() + .trim() + .toLowerCase(); + let status = ''; + if (statusText.includes('en curso') || statusText.includes('ongoing')) { + status = NovelStatus.Ongoing; + } else if ( + statusText.includes('completado') || + statusText.includes('completed') + ) { + status = NovelStatus.Completed; + } else { + status = NovelStatus.Unknown; + } + + // Extraer capítulos + const chapters: Plugin.ChapterItem[] = []; + let chapterIndex = 0; + + $('.vc_row div.vc_column-inner > div.wpb_wrapper').each((i, element) => { + const $el = $(element); + const volume = $el.find('.dt-fancy-title').first().text().trim(); + + if (!volume.startsWith('Volumen')) { + return; + } + + $el.find('.wpb_tab a').each((j, chapterEl) => { + const $chapter = $(chapterEl); + const chapterPartName = $chapter.text().trim(); + const chapterPath = $chapter.attr('href')?.replace(this.site, '') || ''; + + if (!chapterPath) return; + + const match = this.CHAPTER_REGEX.exec(chapterPartName); + let chapterName: string; + + if (match) { + const [, part, chapter, name] = match; + chapterName = `${volume} - ${chapter} - ${part}: ${name}`; + } else { + chapterName = `${volume} - ${chapterPartName}`; + } + + chapters.push({ + name: chapterName, + path: chapterPath, + releaseTime: '', + chapterNumber: chapterIndex + 1, + }); + + chapterIndex++; + }); + }); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name, + cover, + summary, + author, + artist, + status, + chapters, + }; + + return novel; + } + + // Método para obtener contenido del capítulo + async parseChapter(chapterPath: string): Promise<string> { + const url = `${this.site}${chapterPath}`; + const body = await fetchApi(url).then(res => res.text()); + const $ = cheerio.load(body); + + // Determinar el selector correcto basado en el contenido + let $chapterText: cheerio.Cheerio<Element>; + + if (body.includes('Nadie entra sin permiso en la Gran Tumba de Nazarick')) { + $chapterText = $('#content'); + } else { + $chapterText = $('.wpb_text_column.wpb_content_element > .wpb_wrapper'); + } + + // Remover anuncios y elementos no deseados + $chapterText.find('center').remove(); + + // Convertir elementos con text-align center a tags <center> + $chapterText.find('*').each((i, el) => { + const $el = $(el); + const style = $el.attr('style') || ''; + if (/text-align:.?center/.test(style)) { + $el.replaceWith(`<center>${$el.html()}</center>`); + } + }); + + // Aplicar bypass de imágenes de Cloudflare + const chapterContent = await this.bypassCloudflareImages($, $chapterText); + + // Limpiar scripts, estilos y otros elementos innecesarios + const $clean = cheerio.load(chapterContent); + $clean('script, style, iframe, .ads, .advertisement').remove(); + + return $clean.html() || chapterContent; + } +} + +export default new NovaPlugin(); diff --git a/plugins/spanish/hasutl.ts b/plugins/spanish/hasutl.ts new file mode 100644 index 000000000..8f6875dab --- /dev/null +++ b/plugins/spanish/hasutl.ts @@ -0,0 +1,134 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +class HasulTL implements Plugin.PluginBase { + id = 'HasuTL'; + name = 'Hasu Translations'; + icon = 'src/es/hasutl/icon.jpg'; + site = 'https://hasutl.wordpress.com/'; + version = '1.0.0'; + + async popularNovels(): Promise<Plugin.NovelItem[]> { + const url = this.site + 'light-novels-activas/'; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.wp-block-columns').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.wp-block-button').text(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele) + .find('.wp-block-button > a') + .attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + novels.push(novel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('.post-header').text(), + }; + novel.cover = loadedCheerio('.featured-media > img').attr('src'); + + const novelSummary = loadedCheerio('.post-content').find('p').html()!; + novel.summary = novelSummary; + + const novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.wp-block-media-text__content') + .find('a') + .each((idx, ele) => { + const chapterName = loadedCheerio(ele).text().trim(); + + const releaseDate = null; + + const chapterUrl = loadedCheerio(ele).attr('href'); + + if (!chapterUrl) return; + + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }; + + novelChapters.push(chapter); + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('.post-content').html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}?s=${searchTerm}&post_type=wp-manga`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.post-container').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('.post-header').text(); + if ( + !novelName.includes('Cap') && + !novelName.includes('Vol') && + !novelName.includes('Light Novels') + ) { + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + + return novels; + } +} + +export default new HasulTL(); diff --git a/plugins/spanish/novelasligera.ts b/plugins/spanish/novelasligera.ts new file mode 100644 index 000000000..0292e8658 --- /dev/null +++ b/plugins/spanish/novelasligera.ts @@ -0,0 +1,173 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; + +class Ligera implements Plugin.PluginBase { + id = 'novelasligera'; + name = 'Novelas Ligera'; + icon = 'src/es/novelasligera/icon.png'; + site = 'https://novelasligera.com/'; + filters?: Filters | undefined; + version = '1.0.0'; + + async popularNovels(): Promise<Plugin.NovelItem[]> { + const result = await fetchApi(this.site); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.elementor-column').each((idx, ele) => { + const novelName = loadedCheerio(ele) + .find('.widget-image-caption.wp-caption-text') + .text(); + if (novelName) { + const novelCover = loadedCheerio(ele) + .find('a > img') + .attr('data-lazy-src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + + // console.log(url); + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1').text(), + }; + + novel.cover = loadedCheerio('.elementor-widget-container') + .find('img') + .attr('data-lazy-src'); + + loadedCheerio('.elementor-row') + .find('p') + .each(function () { + if (loadedCheerio(this).text().includes('Autor:')) { + novel.author = loadedCheerio(this) + .text() + .replace('Autor:', '') + .trim(); + } + if (loadedCheerio(this).text().includes('Estado:')) { + novel.status = loadedCheerio(this) + .text() + .replace('Estado: ', '') + .trim(); + } + + if (loadedCheerio(this).text().includes('Género:')) { + loadedCheerio(this).find('span').remove(); + novel.genres = loadedCheerio(this).text().replace(/,\s/g, ','); + } + }); + + novel.summary = loadedCheerio( + '.elementor-text-editor.elementor-clearfix', + ).text(); + + const novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.elementor-accordion-item').remove(); + + loadedCheerio('.elementor-tab-content') + .find('li') + .each((idx, ele) => { + const chapterName = loadedCheerio(ele).text(); + const releaseDate = null; + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + if (!chapterUrl) return; + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }; + + novelChapters.push(chapter); + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + // console.log(url); + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + loadedCheerio('.osny-nightmode.osny-nightmode--left').remove(); + loadedCheerio('.code-block.code-block-1').remove(); + loadedCheerio('.adsb30').remove(); + loadedCheerio('.saboxplugin-wrap').remove(); + loadedCheerio('.wp-post-navigation').remove(); + + const chapterText = loadedCheerio('.entry-content').html() || ''; + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = this.site + '?s=' + searchTerm + '&post_type=wp-manga'; + // console.log(url); + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('.inside-article').each((idx, ele) => { + const novelCover = loadedCheerio(ele).find('img').attr('src'); + let novelUrl = loadedCheerio(ele).find('a').attr('href'); + + let novelName; + + if (novelUrl) { + novelName = novelUrl.replace(/-/g, ' ').replace(/^./, function (x) { + return x.toUpperCase(); + }); + } + + novelUrl += '/'; + + if (!novelName || !novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + novels = [{ ...novels[1] }]; + + return novels; + } +} + +export default new Ligera(); diff --git a/plugins/spanish/novelawuxia.ts b/plugins/spanish/novelawuxia.ts new file mode 100644 index 000000000..c4150f9b5 --- /dev/null +++ b/plugins/spanish/novelawuxia.ts @@ -0,0 +1,198 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { Filters } from '@libs/filterInputs'; + +class ReinoWuxia implements Plugin.PluginBase { + id = 'reinowuxia'; + name = 'ReinoWuxia'; + icon = 'src/es/reinowuxia/icon.png'; + filters?: Filters | undefined; + version = '1.0.0'; + site = 'http://www.reinowuxia.com/'; + getNovelName(y: string | undefined) { + return y?.replace(/-/g, ' ').replace(/(?:^|\s)\S/g, a => a.toUpperCase()); + } + async popularNovels(): Promise<Plugin.NovelItem[]> { + const url = this.site + 'p/todas-las-novelas.html'; + + const result = await fetchApi(url, { + method: 'GET', + }); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.post-body.entry-content') + .find('a') + .each((idx, ele) => { + let novelName = loadedCheerio(ele) + .attr('href') + ?.split('/') + .pop() + ?.replace('.html', ''); + novelName = this.getNovelName(novelName); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).attr('href'); + + if (!novelName || !novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.post-title').text().trim(), + }; + + novel.cover = loadedCheerio('div.separator').find('a').attr('href'); + + novel.status = ''; + + loadedCheerio('div > b').each(function () { + const detailName = loadedCheerio(this).text(); + const detail = loadedCheerio(this)[0].nextSibling; + + if (detailName && detail) { + const text = loadedCheerio(detail).text(); + + if (detailName.includes('Autor')) { + novel.author = text.replace('Autor:', ''); + } + + if (detailName.includes('Estatus')) { + novel.status = text.replace('Estatus: ', ''); + } + if (detailName.includes('Géneros:')) { + novel.genres = text.replace('Géneros: ', '').replace(/,\s/g, ','); + } + } + }); + + const novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('div').each((idx, rootEle) => { + const detailName = loadedCheerio(rootEle).text(); + if (detailName.includes('Sinopsis')) { + novel.summary = + loadedCheerio(rootEle).next().text() !== '' + ? loadedCheerio(rootEle) + .next() + .text() + .replace('Sinopsis', '') + .trim() + : loadedCheerio(rootEle) + .next() + .next() + .text() + .replace('Sinopsis', '') + .trim(); + } + + if (detailName.includes('Lista de Capítulos')) { + loadedCheerio(rootEle) + .find('a') + .each((idx, ele) => { + const chapterName = loadedCheerio(ele).text(); + const chapterPath = loadedCheerio(ele) + .attr('href') + ?.replace(this.site, ''); + const releaseDate = null; + + if ( + chapterName && + chapterPath && + chapterPath !== '/' && + !novelChapters.some(chap => chap.name === chapterName) + ) { + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterPath, + }; + + novelChapters.push(chapter); + } + }); + } + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('.post-body.entry-content').html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}search?q=${searchTerm}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.date-outer').each((idx, ele) => { + let novelName = loadedCheerio(ele) + .find('a') + .attr('href') + ?.split('/') + .pop() + ?.replace(/-capitulo(.*?).html/, ''); + + const novelUrl = novelName + '.html/'; + + novelName = this.getNovelName(novelName); + + const exists = novels.some(novel => novel.name === novelName); + + if (!exists) { + const novelCover = defaultCover; + + if (!novelUrl || !novelName) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + + return novels; + } +} + +export default new ReinoWuxia(); diff --git a/plugins/spanish/novelyra.ts b/plugins/spanish/novelyra.ts new file mode 100644 index 000000000..87ea19f53 --- /dev/null +++ b/plugins/spanish/novelyra.ts @@ -0,0 +1,323 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { CheerioAPI, load as loadCheerio } from 'cheerio'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +function parseSpanishTextToISO(text: string) { + if (!text) return null; + + const now = new Date(); + const textLower = text.trim().toLowerCase(); + + // --- 1. MAPEOS Y EXPRESIONES REGULARES --- + const months = { + enero: 0, + febrero: 1, + marzo: 2, + abril: 3, + mayo: 4, + junio: 5, + julio: 6, + agosto: 7, + septiembre: 8, + octubre: 9, + noviembre: 10, + diciembre: 11, + }; + + // Expresión para: "21 de febrero de 2026" + const absoluteRegex = /^(\d{1,2})\s+de\s+([a-z]+)\s+de\s+(\d{4})$/i; + + // --- 2. EVALUAR FECHAS ABSOLUTAS --- + const absoluteMatch = textLower.match(absoluteRegex); + if (absoluteMatch) { + const day = parseInt(absoluteMatch[1], 10); + const monthStr = absoluteMatch[2]; + const year = parseInt(absoluteMatch[3], 10); + + const monthIndex = months[monthStr as keyof typeof months]; + if (monthIndex !== undefined) { + // Se crea la fecha en hora local (00:00:00) + const date = new Date(year, monthIndex, day); + return date.toISOString(); + } + } + + // --- 3. EVALUAR FECHAS RELATIVAS ("hace...") --- + if (textLower.startsWith('hace')) { + // Normalizar "un" / "una" a "1" para facilitar el cálculo + const normalized = textLower + .replace(/\b(un|una)\b/g, '1') + .replace('un momento', '0 segundos'); + + if (normalized.includes('momento')) { + return now.toISOString(); + } + + const relativeRegex = /(\d+)\s+([a-zñáéíóú]+)/i; + const relativeMatch = normalized.match(relativeRegex); + + if (relativeMatch) { + const value = parseInt(relativeMatch[1], 10); + const unit = relativeMatch[2]; + + const date = new Date(now); // Clonamos la fecha actual + + if (unit.startsWith('segundo')) { + date.setSeconds(date.getSeconds() - value); + } else if (unit.startsWith('minuto')) { + date.setMinutes(date.getMinutes() - value); + } else if (unit.startsWith('hora')) { + date.setHours(date.getHours() - value); + } else if (unit.startsWith('dia') || unit.startsWith('día')) { + date.setDate(date.getDate() - value); + } else if (unit.startsWith('mes')) { + date.setMonth(date.getMonth() - value); + } else if (unit.startsWith('año') || unit.startsWith('ano')) { + date.setFullYear(date.getFullYear() - value); + } + + return date.toISOString(); + } + } + + // Si no coincide con ningún formato conocido, intentar el parse nativo o lanzar error + try { + const fallbackDate = new Date(text); + if (!isNaN(fallbackDate.getTime())) return fallbackDate.toISOString(); + } catch (e) { + // No se pudo parsear + } + + throw new Error(`Formato de fecha no soportado: "${text}"`); +} + +class Novelyra implements Plugin.PluginBase { + id = 'novelyra'; + name = 'Novelyra'; + icon = 'src/es/novelyra/icon.png'; + site = 'https://novelyra.com/'; + version = '1.0.1'; + filters: Filters = { + genres: { + type: FilterTypes.Picker, + label: 'Generos', + value: '', + options: [ + { label: 'Todos', value: '' }, + { label: 'Acción', value: 'accion' }, + { label: 'Aventura', value: 'aventura' }, + { label: 'Fantasía', value: 'fantasia' }, + { label: 'Artes Marciales', value: 'artes-marciales' }, + { label: 'Harén', value: 'haren' }, + { label: 'Romance', value: 'romance' }, + { label: 'Sobrenatural', value: 'sobrenatural' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Comedia', value: 'comedia' }, + { label: 'Ciencia Ficción', value: 'ciencia-ficcion' }, + { label: 'Misterio', value: 'misterio' }, + { label: 'Maduro', value: 'maduro' }, + { label: 'Psicológico', value: 'psicologico' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Reencarnación', value: 'reencarnacion' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Vida Escolar', value: 'vida-escolar' }, + { label: 'Josei', value: 'josei' }, + { label: 'Drama', value: 'drama' }, + { label: 'Urbano', value: 'urbano' }, + { label: 'Oriental', value: 'oriental' }, + { label: 'Horror', value: 'horror' }, + { label: 'Tragedia', value: 'tragedia' }, + { label: 'Juegos', value: 'juegos' }, + ], + }, + browse: { + type: FilterTypes.Picker, + label: 'Novelas Populares', + value: 'browse.php', + options: [ + { label: 'Todas las Novelas', value: 'browse.php' }, + { label: '🔥 Hoy', value: 'popular.php?period=today' }, + { label: '📅 Este Mes', value: 'popular.php?period=month' }, + { label: '👑 De Siempre', value: 'popular.php?period=alltime' }, + ], + }, + } satisfies Filters; + + private loadNovels( + loadedCheerio: CheerioAPI, + typeNovel: string, + ): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(typeNovel).each((_, ele) => { + const novel = loadedCheerio(ele); + novels.push({ + name: novel.find('h3').text(), + path: novel.find('a').attr('href')?.replace(this.site, '') || '', + cover: novel.find('img').attr('src') || defaultCover, + }); + }); + + return novels; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site; + let typeNovel = '#novelas .novel-card'; + const genre = filters.genres?.value as string; + const browse = filters.browse?.value as string; + if (!showLatestNovels) { + if (browse.startsWith('popular.php')) { + url = `${this.site}${browse}`; + typeNovel = '.popular-item'; + } else { + const params = new URLSearchParams(); + params.append('page', String(pageNo)); + if (genre) { + params.append('genre', genre); + } + url = `${this.site}${browse}?${params.toString()}`; + typeNovel = '.novels-grid .novel-card'; + } + } + + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = loadCheerio(body); + + return this.loadNovels(loadedCheerio, typeNovel); + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const result = await fetchApi(this.site + novelPath); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1').text(), + }; + + novel.cover = loadedCheerio('img').attr('src') || defaultCover; + novel.genres = loadedCheerio('.novel-meta .novel-genres') + .text() + .trim() + .replace('\n', ', '); + + novel.status = NovelStatus.Completed; + novel.summary = loadedCheerio('.novel-description-detail').text().trim(); + + const chapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.chapter-item-wrapper').each((idx, ele) => { + const cptr = loadedCheerio(ele); + const numberText = cptr.find('.chapter-number').text(); + const numberMatch = numberText.match(/(\d+)/); + const chapterNumber = numberMatch ? parseInt(numberMatch[1]) : 0; + const chapter: Plugin.ChapterItem = { + name: cptr.find('.chapter-title').text(), + path: cptr.find('a').attr('href')?.replace(this.site, '') || '', + releaseTime: parseSpanishTextToISO(cptr.find('.chapter-date').text()), + chapterNumber: chapterNumber, + }; + chapters.push(chapter); + }); + + novel.chapters = chapters; + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const myHeaders = new Headers(); + myHeaders.set( + 'User-Agent', + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36', + ); + myHeaders.set( + 'Accept', + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8', + ); + myHeaders.set('Accept-Language', 'es-ES,es;q=0.9'); + myHeaders.set('Referer', this.site); + myHeaders.set('Cache-Control', 'no-cache'); + + const result = await fetchApi(this.site + chapterPath, { + method: 'GET', + headers: myHeaders, + }); + + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + // Quita scripts + loadedCheerio('script').remove(); + // Quita bloques de anuncios + loadedCheerio('.chapter-ad').remove(); + // Quita tags de adsense si los hay + loadedCheerio('ins').remove(); + + const chapterText = loadedCheerio('.chapter-content'); + let paragraph: string[] = []; + const chapterHtml: string[] = []; + const tagsPermisive: string[] = ['b', 'i', 'u', 'strong', 'em', 'span']; + + chapterText.contents().each((_, element) => { + switch (element.type) { + case 'text': + if (element.data.trim()) { + paragraph.push(element.data.trim()); + } + break; + case 'tag': + { + const originalTag = element.tagName; + if (tagsPermisive.includes(originalTag)) { + paragraph.push(loadedCheerio.html(element)); + } else { + if (paragraph.length > 0) { + chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`); + paragraph = []; + if (originalTag === 'br') break; + } + chapterHtml.push(loadedCheerio.html(element)); + } + } + break; + } + }); + // Close any remaining paragraph + if (paragraph.length > 0) { + chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`); + } + return chapterHtml.join(''); + } + async searchNovels( + searchTerm: string, + // pageNo: number, + ): Promise<Plugin.NovelItem[]> { + searchTerm = searchTerm.toLowerCase(); + + const url = `${this.site}?search=${encodeURIComponent(searchTerm)}`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = loadCheerio(body); + + const typeNovel = '#novelas .novel-card'; + + return this.loadNovels(loadedCheerio, typeNovel); + } +} + +export default new Novelyra(); diff --git a/plugins/spanish/oasistranslations.ts b/plugins/spanish/oasistranslations.ts new file mode 100644 index 000000000..9b677c060 --- /dev/null +++ b/plugins/spanish/oasistranslations.ts @@ -0,0 +1,155 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; +class Oasis implements Plugin.PluginBase { + id = 'oasistranslations'; + name = 'Oasis Translations'; + site = 'https://oasistranslations.wordpress.com/'; + version = '1.0.0'; + filters?: Filters | undefined; + icon = 'src/es/oasistranslations/icon.png'; + async popularNovels(): Promise<Plugin.NovelItem[]> { + const result = await fetchApi(this.site); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.menu-item-1819') + .find('.sub-menu > li') + .each((idx, ele) => { + const novelName = loadedCheerio(ele).text(); + if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title') + .text() + .replace(/[\t\n]/g, '') + .trim(), + }; + novel.cover = loadedCheerio('img[loading="lazy"]').attr('src'); + + loadedCheerio('.entry-content > p').each(() => { + if (loadedCheerio(this).text().includes('Autor')) { + const details = loadedCheerio(this) + .html() + ?.match(/<\/strong>(.|\n)*?<br>/g) + ?.map(detail => detail.replace(/<strong>|<\/strong>|<br>|:\s/g, '')); + + novel.genres = ''; + if (details) { + novel.author = details[2]; + novel.genres = details[4].replace(/\s| /g, ''); + } + } + }); + + // let novelSummary = $(this).next().html(); + novel.summary = ''; + + const novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('.entry-content') + .find('a') + .each((idx, ele) => { + const chapterUrl = loadedCheerio(ele).attr('href'); + + if (chapterUrl && chapterUrl.includes(this.site)) { + const chapterName = loadedCheerio(ele).text(); + const releaseDate = null; + + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }; + + novelChapters.push(chapter); + } + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + loadedCheerio('div#jp-post-flair').remove(); + + const chapterText = loadedCheerio('.entry-content').html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + searchTerm = searchTerm.toLowerCase(); + + const result = await fetchApi(this.site); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + loadedCheerio('.menu-item-1819') + .find('.sub-menu > li') + .each((idx, ele) => { + const novelName = loadedCheerio(ele).text(); + if (!novelName.match(/Activas|Finalizadas|Dropeadas/)) { + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + } + }); + + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm), + ); + + return novels; + } +} + +export default new Oasis(); diff --git a/plugins/spanish/skynovels.ts b/plugins/spanish/skynovels.ts new file mode 100644 index 000000000..b2b31a21e --- /dev/null +++ b/plugins/spanish/skynovels.ts @@ -0,0 +1,315 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; + +class SkyNovels implements Plugin.PluginBase { + id = 'skynovels'; + name = 'SkyNovels'; + site = 'https://www.skynovels.net/'; + apiSite = 'https://api.skynovels.net/api/'; + version = '1.1.0'; + icon = 'src/es/skynovels/icon.png'; + filters = { + genres: { + type: FilterTypes.CheckboxGroup, + label: 'Generos', + value: [], + options: [ + { label: 'Acción', value: '9' }, + { label: 'Adulto', value: '38' }, + { label: 'Artes marciales', value: '3' }, + { label: 'Aventura', value: '2' }, + { label: 'BL', value: '40' }, + { label: 'Comedia', value: '7' }, + { label: 'Cosas de la vida', value: '26' }, + { label: 'Cultivación', value: '19' }, + { label: 'Drama', value: '8' }, + { label: 'Ecchi', value: '21' }, + { label: 'Fantasia', value: '4' }, + { label: 'Gender Bender', value: '10' }, + { label: 'GL', value: '41' }, + { label: 'Harem', value: '12' }, + { label: 'Histórico', value: '32' }, + { label: 'Horror', value: '39' }, + { label: 'LitRPG', value: '31' }, + { label: 'Maduro', value: '1' }, + { label: 'Magia', value: '16' }, + { label: 'Misterio', value: '22' }, + { label: 'Mundo Moderno', value: '34' }, + { label: 'Psicológico', value: '27' }, + { label: 'Recuentos de la vida', value: '36' }, + { label: 'Reencarnación', value: '23' }, + { label: 'Romance', value: '5' }, + { label: 'Sci-Fi', value: '17' }, + { label: 'Seinen', value: '18' }, + { label: 'Shoujo', value: '33' }, + { label: 'Shounen', value: '13' }, + { label: 'Sobrenatural', value: '20' }, + { label: 'Supervivencia', value: '25' }, + { label: 'Suspenso', value: '35' }, + { label: 'Tragedia', value: '14' }, + { label: 'Transmigración', value: '24' }, + { label: 'Vida Escolar', value: '29' }, + { label: 'Xianxia', value: '6' }, + { label: 'Xuanhuan', value: '11' }, + { label: 'Yaoi', value: '30' }, + { label: 'Sin género indicado', value: '37' }, + ], + }, + } satisfies Filters; + + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const genres = (filters?.genres?.value as string[]) || []; + const order = genres.length > 0 ? 'updated' : 'rating'; + let url = `${this.apiSite}novels?page=${pageNo}&order=${order}`; + if (genres.length > 0) url += `&genres=${genres.join(',')}`; + + const result = await fetchApi(url, { + headers: { + 'Cache-Control': 'no-cache', + }, + }); + const body = (await result.json()) as response; + + const novels: Plugin.NovelItem[] = []; + + body.novels?.forEach(res => { + const name = res.nvl_title; + const cover = this.apiSite + 'get-image/' + res.image + '/novels/false'; + const path = 'novelas/' + res.id + '/' + res.nvl_name + '/'; + + novels.push({ name, cover, path }); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novelId = novelPath.split('/')[1]; + const url = this.apiSite + 'novel/' + novelId + '/reading?&q'; + + const result = await fetchApi(url, { + headers: { + 'Cache-Control': 'no-cache', + }, + }); + const body = (await result.json()) as responseBook; + + const item = body?.novel?.[0]; + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: item?.nvl_title || 'Untitled', + }; + + novel.cover = this.apiSite + 'get-image/' + item?.image + '/novels/false'; + + const genres: string[] = []; + item?.genres?.forEach(genre => genres.push(genre.genre_name)); + novel.genres = genres.join(','); + novel.author = item?.nvl_writer; + novel.summary = item?.nvl_content; + novel.status = item?.nvl_status; + + const novelChapters: Plugin.ChapterItem[] = []; + + item?.volumes?.forEach(volume => { + volume?.chapters?.forEach(chapter => { + const chapterName = chapter.chp_index_title; + const releaseDate = new Date(chapter.createdAt).toDateString(); + const chapterPath = novelPath + chapter.id + '/' + chapter.chp_name; + + novelChapters.push({ + name: chapterName, + releaseTime: releaseDate, + path: chapterPath, + }); + }); + }); + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const chapterId: string = chapterPath.split('/')[3]; + const url = `${this.apiSite}novel-chapter/${chapterId}`; + + const result = await fetchApi(url, { + headers: { + 'Cache-Control': 'no-cache', + }, + }); + const body = (await result.json()) as responseChapter; + + const item = body?.chapter?.[0]; + + const chapterText = item?.chp_content || ''; + + return chapterText.replace(/\n/g, '<br>'); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + searchTerm = encodeURIComponent(searchTerm.toLowerCase()); + const url = `${this.apiSite}novels?page=${pageNo}&q=${searchTerm}`; + + const result = await fetchApi(url, { + headers: { + 'Cache-Control': 'no-cache', + }, + }); + const body = (await result.json()) as response; + + const novels: Plugin.NovelItem[] = []; + + body?.novels?.forEach(res => { + const name = res.nvl_title; + const cover = this.apiSite + 'get-image/' + res.image + '/novels/false'; + const path = 'novelas/' + res.id + '/' + res.nvl_name + '/'; + + novels.push({ name, cover, path }); + }); + + return novels; + } +} + +export default new SkyNovels(); + +type response = { + novels?: NovelsEntity[] | null; +}; +type NovelsEntity = { + id: number; + nvl_author?: number | null; + nvl_content: string; + nvl_title: string; + nvl_acronym?: string | null; + nvl_status: string; + nvl_publication_date?: string | null; + nvl_name: string; + nvl_recommended: number; + nvl_writer: string; + nvl_translator?: string | null; + nvl_translator_eng?: string | null; + image: string; + createdAt: string; + updatedAt: string; + nvl_chapters: number; + nvl_last_update: string; + nvl_rating?: number | null; + nvl_ratings_count: number; + genres?: GenresEntity[] | null; +}; +type GenresEntity = { + id: number; + genre_name: string; +}; + +type responseBook = { + novel?: NovelEntity[] | null; +}; +type NovelEntity = { + id: number; + nvl_author: number; + nvl_content: string; + nvl_title: string; + nvl_acronym: string; + nvl_status: string; + nvl_publication_date: string; + nvl_name: string; + nvl_recommended: number; + nvl_writer: string; + nvl_translator: string; + nvl_translator_eng: string; + image: string; + createdAt: string; + updatedAt: string; + nvl_chapters: number; + nvl_last_update: string; + nvl_rating: number; + bookmarks?: BookmarksEntity[] | null; + volumes?: VolumesEntity[] | null; + novel_ratings?: NovelRatingsEntity[] | null; + collaborators?: CollaboratorsEntity[] | null; + genres?: GenresEntity[] | null; +}; +type BookmarksEntity = { + id: number; + user_id: number; + chp_id: number; + chp_name: string; +}; +type VolumesEntity = { + vlm_title: string; + id: number; + nvl_id: number; + user_id?: number | null; + chapters?: ChaptersEntity[] | null; +}; +type ChaptersEntity = { + id: number; + chp_index_title: string; + chp_name: string; + chp_number: number; + chp_status: string; + createdAt: string; +}; +type NovelRatingsEntity = { + user_id: number; + rate_value: number; + rate_comment: string; + replys_count: string; + createdAt: string; + updatedAt: string; + id: number; + user_login: string; + image?: string | null; + likes?: (LikesEntity | null)[] | null; +}; +type LikesEntity = { + id: number; + user_id: number; + user_login: string; +}; +type CollaboratorsEntity = { + user_id: number; + user_login: string; +}; + +type responseChapter = { + chapter?: ChapterEntity[] | null; +}; +type ChapterEntity = { + id: number; + chp_author: number; + chp_translator?: null; + nvl_id: number; + vlm_id: number; + chp_number: number; + chp_content: string; + chp_review?: null; + chp_title?: null; + chp_index_title: string; + chp_status: string; + chp_name: string; + createdAt: string; + updatedAt: string; + nvl_title: string; + nvl_name: string; + user_login: string; + reactions_count: number; + comments?: null[] | null; + reactions?: null[] | null; + total_reactions?: TotalReactionsEntity[] | null; +}; +type TotalReactionsEntity = { + reaction_id: number; + reaction_name: string; + reaction_count: number; +}; diff --git a/plugins/spanish/tunovelaligera.ts b/plugins/spanish/tunovelaligera.ts new file mode 100644 index 000000000..f5712bba2 --- /dev/null +++ b/plugins/spanish/tunovelaligera.ts @@ -0,0 +1,349 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { defaultCover } from '@libs/defaultCover'; +import { NovelStatus } from '@libs/novelStatus'; + +class TuNovelaLigera implements Plugin.PagePlugin { + id = 'tunovelaligera'; + name = 'TuNovelaLigera'; + icon = 'src/es/tunovelaligera/icon.png'; + site = 'https://tunovelaligera.com'; + version = '1.2.1'; + + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + async popularNovels( + pageNo: number, + { + // showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site; + const isFilterGenres = filters?.genres?.value != ''; + const isFilterOrder = filters?.order?.value != 'rating'; + link += isFilterOrder + ? `/novelas/?m_orderby=${filters?.order?.value}` + : `/wp-admin/admin-ajax.php`; + + const formData = new FormData(); + if (!isFilterOrder) { + formData.append('action', 'madara_load_more'); + formData.append('page', pageNo.toString()); + formData.append('template', 'madara-core/content/content-archive'); + formData.append('vars[post_type]', 'wp-manga'); + + if (isFilterGenres) { + formData.append('vars[wp-manga-genre]', filters.genres.value); + } + } + + const result = await fetchApi( + link, + isFilterOrder + ? {} + : { + method: 'POST', + body: formData, + }, + ).then(res => res.text()); + + const loadedCheerio = parseHTML(result); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.page-item-detail').each((i, el) => { + const name = loadedCheerio(el).find('.h5 > a').text(); + const cover = loadedCheerio(el).find('img').attr('src'); + const url = loadedCheerio(el).find('.h5 > a').attr('href'); + if (!url) return; + + novels.push({ name, cover, path: url.replace(this.site, '') }); + }); + + return novels; + } + + parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] { + const hasWpMangaItems = + loadedCheerio("ul > li[class*='wp-manga'][class^='wp-manga']").length > 0; + + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio( + hasWpMangaItems + ? "ul > li[class*='wp-manga'][class^='wp-manga']" + : "ul[id*='lcp_instance'] > li", + ).each((_i, el) => { + const chapterName = loadedCheerio(el) + .find('a') + .text() + .replace(/[\t\n]/g, '') + .trim(); + + const chapterUrl = loadedCheerio(el).find('a').attr('href'); + if (!chapterUrl) return; + chapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + }); + }); + return chapters; + } + + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const novelUrl = this.site + novelPath; + const result = await fetchApi(novelUrl); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const nameNovel = novelPath.split('/')[2]; + + const apiChapter = `${this.site}/novelas/${nameNovel}`; + const resultApi = await fetchApi(`${apiChapter}/ajax/chapters/`, { + method: 'POST', + }); + const bodyApi = await resultApi.text(); + const chaptersApi = parseHTML(bodyApi); + + const hasWpMangaItems = + chaptersApi("ul > li[class*='wp-manga'][class^='wp-manga']").length > 0; + + let lastPage = 1; + loadedCheerio("ul[id*='lcp_paginator'] > li > a").each(function () { + const page = Number(this.attribs['title']); + if (page && page > lastPage) lastPage = page; + }); + + const novel: Plugin.SourceNovel & { + totalPages: number; + latestChapter?: Plugin.ChapterItem; + } = { + path: novelPath, + chapters: [], + totalPages: hasWpMangaItems ? 0 : lastPage, + name: loadedCheerio('.post-title > h1').text().trim(), + latestChapter: undefined, + }; + + loadedCheerio('.manga-title-badges').remove(); + + const novelCover = loadedCheerio('.summary_image > a > img'); + + novel.cover = ( + novelCover.attr('data-src') || + novelCover.attr('src') || + novelCover.attr('data-cfsrc') || + defaultCover + ).trim(); + + loadedCheerio('.post-content_item').each(function () { + const detailName = loadedCheerio(this) + .find('.summary-heading > h5') + .text() + .trim(); + const detail = loadedCheerio(this).find('.summary-content').text().trim(); + + switch (detailName) { + case 'Género(s)': + novel.genres = detail.replace(/, /g, ','); + break; + case 'Autor(es)': + novel.author = detail; + break; + case 'Estado': + novel.status = + detail.includes('OnGoing') || detail.includes('Updating') + ? NovelStatus.Ongoing + : NovelStatus.Completed; + break; + } + }); + + novel.summary = loadedCheerio('div.summary__content > p').text().trim(); + + const cherioDefault = hasWpMangaItems ? chaptersApi : loadedCheerio; + + novel.chapters = this.parseChapters(cherioDefault); + + if (!hasWpMangaItems) { + const latestChapterEle = loadedCheerio( + `ul[id*='lcp_instance'] > li`, + ).first(); + const latestChapterUrl = loadedCheerio(latestChapterEle) + .find('a') + .attr('href'); + const latestChapterName = loadedCheerio(latestChapterEle) + .find('a') + .text() + .replace(/[\t\n]/g, '') + .trim(); + novel.latestChapter = latestChapterUrl + ? { + path: latestChapterUrl.replace(this.site, ''), + name: latestChapterName, + } + : undefined; + } + + return novel; + } + + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + novelPath = novelPath.replace(/=[0-9]+/, '='); + const pageUrl = this.site + novelPath + page; + const pageText = await fetchApi(pageUrl).then(res => res.text()); + const chapters = this.parseChapters(parseHTML(pageText)); + return { + chapters, + }; + } + + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio( + 'div.entry-content_wrap:has(>p), div.entry-content_wrap div:has(>p)', + ).first(); + + chapterText.children().not('p').remove(); + + let paragraph: string[] = []; + const chapterHtml: string[] = []; + const tagsPermisive: string[] = ['b', 'i', 'u', 'strong', 'em', 'span']; + + chapterText.contents().each((_, element) => { + switch (element.type) { + case 'text': + if (element.data.trim()) { + paragraph.push(element.data.trim()); + } + break; + case 'tag': + { + const originalTag = element.tagName; + if (tagsPermisive.includes(originalTag)) { + paragraph.push(loadedCheerio.html(element)); + } else { + if (paragraph.length > 0) { + chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`); + paragraph = []; + if (originalTag === 'br') break; + } + chapterHtml.push(loadedCheerio.html(element)); + } + } + break; + } + }); + // Close any remaining paragraph + if (paragraph.length > 0) { + chapterHtml.push(`<p>${paragraph.join(' ').trim()}</p>`); + } + return chapterHtml.join(''); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/?s=${encodeURIComponent(searchTerm)}&post_type=wp-manga`; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.c-tabs-item__content').each((i, el) => { + const name = loadedCheerio(el).find('.h4 > a').text(); + const cover = loadedCheerio(el).find('img').attr('src'); + const url = loadedCheerio(el).find('.h4 > a').attr('href'); + if (!url) return; + + novels.push({ name, cover, path: url.replace(this.site, '') }); + }); + + return novels; + } + + filters = { + order: { + value: 'rating', + label: 'Ordenado por', + options: [ + // ! not working + // { label: 'Lo mas reciente', value: 'latest_update' }, + { label: 'A-Z', value: 'alphabet' }, + { label: 'Clasificación', value: 'rating' }, + { label: 'Trending', value: 'trending' }, + { label: 'Mas visto', value: 'views' }, + { label: 'Nuevo', value: 'new-manga' }, + ], + type: FilterTypes.Picker, + }, + genres: { + value: '', + label: 'Generos', + options: [ + { label: 'None', value: '' }, + { label: 'Acción', value: 'accion' }, + { label: 'Adulto', value: 'adulto' }, + { label: 'Artes Marciales', value: 'artes-marciales' }, + { label: 'Aventura', value: 'aventura' }, + { label: 'Ciencia Ficción', value: 'ciencia-ficcion' }, + { label: 'Comedia', value: 'comedia' }, + { label: 'Deportes', value: 'deportes' }, + { label: 'Drama', value: 'drama' }, + { label: 'Eastern Fantasy', value: 'eastern-fantasy' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'FanFiction', value: 'fan-fiction' }, + { label: 'Fantasía', value: 'fantasia' }, + { label: 'Fantasía oriental', value: 'fantasia-oriental' }, + { label: 'Ficción Romántica', value: 'ficcion-romantica' }, + { label: 'Gender Bender', value: 'gender-bender' }, + { label: 'Harem', value: 'harem' }, + { label: 'Histórico', value: 'historico' }, + { label: 'Horror', value: 'horror' }, + { label: 'Josei', value: 'josei' }, + { label: 'Maduro', value: 'maduro' }, + { label: 'Mecha', value: 'mecha' }, + { label: 'Misterio', value: 'misterio' }, + { label: 'Novela China', value: 'novela-china' }, + { label: 'Novela FanFiction', value: 'novela-fanfiction' }, + { label: 'Novela Japonesa', value: 'novela-japonesa' }, + { label: 'Novela Koreana', value: 'novela-koreana' }, + { label: 'Novela ligera', value: 'novela-ligera' }, + { label: 'Novela original', value: 'novela-original' }, + { label: 'Novela Web', value: 'web-novel' }, + { label: 'Psicológico', value: 'psicologico' }, + { label: 'Realismo Mágico', value: 'realismo-magico' }, + { label: 'Recuento de vida', value: 'recuento-de-vida' }, + { label: 'Romance', value: 'romance' }, + { label: 'Romance contemporáneo', value: 'romance-contemporaneo' }, + { label: 'Romance Moderno', value: 'romance-moderno' }, + { label: 'Seinen', value: 'seinen' }, + { label: 'Shoujo', value: 'shoujo' }, + { label: 'Shounen', value: 'shounen' }, + { label: 'Sobrenatural', value: 'sobrenatural' }, + { label: 'Tragedia', value: 'tragedia' }, + { label: 'Vampiros', value: 'vampiros' }, + { label: 'Vida Escolar', value: 'vida-escolar' }, + { label: 'Western Fantasy', value: 'western-fantasy' }, + { label: 'Wuxia', value: 'wuxia' }, + { label: 'Xianxia', value: 'xianxia' }, + { label: 'Xuanhuan', value: 'xuanhuan' }, + { label: 'Yaoi', value: 'yaoi' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new TuNovelaLigera(); diff --git a/plugins/spanish/yukitls.ts b/plugins/spanish/yukitls.ts new file mode 100644 index 000000000..63ab695f2 --- /dev/null +++ b/plugins/spanish/yukitls.ts @@ -0,0 +1,174 @@ +import { fetchApi } from '@libs/fetch'; +import { Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; +import { load as parseHTML } from 'cheerio'; + +class Yuuki implements Plugin.PluginBase { + id = 'yuukitls'; + name = 'Yuuki Tls'; + icon = 'src/es/yuukitls/icon.png'; + site = 'https://yuukitls.com/'; + filters?: Filters | undefined; + version = '1.0.0'; + + async popularNovels(): Promise<Plugin.NovelItem[]> { + const result = await fetchApi(this.site); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.quadmenu-navbar-collapse ul li:nth-child(2)') + .find('li') + .each((idx, ele) => { + const novelName = loadedCheerio(ele) + .text() + .replace(/[\s\n]+/g, ' '); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + if (!novelUrl) return; + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1.entry-title') + .text() + .replace(/[\t\n]/g, '') + .trim(), + }; + + novel.cover = loadedCheerio('img[loading="lazy"]').attr('src'); + + loadedCheerio('.entry-content') + .find('div') + .each(function () { + if (loadedCheerio(this).text().includes('Escritor:')) { + novel.author = loadedCheerio(this) + .text() + .replace('Escritor: ', '') + .trim(); + } + if (loadedCheerio(this).text().includes('Género:')) { + novel.genres = loadedCheerio(this) + .text() + .replace(/Género: |\s/g, ''); + } + + if (loadedCheerio(this).text().includes('Sinopsis:')) { + novel.summary = loadedCheerio(this).next().text(); + } + }); + + const novelChapters: Plugin.ChapterItem[] = []; + + if (loadedCheerio('.entry-content').find('li').length) { + loadedCheerio('.entry-content') + .find('li') + .each((idx, ele) => { + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + + if (chapterUrl && chapterUrl.includes(this.site)) { + const chapterName = loadedCheerio(ele).text(); + const releaseDate = null; + + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }; + + novelChapters.push(chapter); + } + }); + } else { + loadedCheerio('.entry-content') + .find('p') + .each((idx, ele) => { + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + + if (chapterUrl && chapterUrl.includes(this.site)) { + const chapterName = loadedCheerio(ele).text(); + const releaseDate = null; + + const chapter = { + name: chapterName, + releaseTime: releaseDate, + path: chapterUrl.replace(this.site, ''), + }; + + novelChapters.push(chapter); + } + }); + } + + novel.chapters = novelChapters; + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('.entry-content').html() || ''; + + return chapterText; + } + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + searchTerm = searchTerm.toLowerCase(); + + const result = await fetchApi(this.site); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let novels: Plugin.NovelItem[] = []; + + loadedCheerio('.menu-item-2869') + .find('.menu-item.menu-item-type-post_type.menu-item-object-post') + .each((idx, ele) => { + const novelName = loadedCheerio(ele).text(); + const novelCover = loadedCheerio(ele).find('img').attr('src'); + + const novelUrl = loadedCheerio(ele).find('a').attr('href'); + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + novels = novels.filter(novel => + novel.name.toLowerCase().includes(searchTerm), + ); + + return novels; + } +} + +export default new Yuuki(); diff --git a/plugins/thai/.gitkeep b/plugins/thai/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/turkish/MangaTR.ts b/plugins/turkish/MangaTR.ts new file mode 100644 index 000000000..23525d94a --- /dev/null +++ b/plugins/turkish/MangaTR.ts @@ -0,0 +1,384 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class MangaTR implements Plugin.PluginBase { + id = 'mangatr'; + name = 'MangaTR'; + icon = 'src/tr/mangatr/icon.png'; + site = 'https://manga-tr.com/'; + version = '1.0.1'; + + opts = { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'x-requested-with': 'XMLHttpRequest', + }, + }; + + /** + * @param novelPath + * @returns novel metadata and its first page + */ + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const body = await fetchApi(this.site + novelPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('#tables').text(), + cover: loadedCheerio( + '#myCarousel > div.container > div.col-lg-4.col-sm-4 > img', + ).attr('src'), + status: loadedCheerio( + '#tab1 > table:nth-child(2) > tbody > tr:nth-child(2) > td:nth-last-child(2) > a', + ).text(), + chapters: [], + author: loadedCheerio( + '#tab1 > table:nth-child(3) > tbody > tr:nth-child(2) > td:nth-child(1) > a', + ) + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(','), + artist: loadedCheerio( + '#tab1 > table:nth-child(3) > tbody > tr:nth-child(2) > td:nth-child(2) > a', + ) + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(','), + genres: loadedCheerio( + '#tab1 > table:nth-child(3) > tbody > tr:nth-child(2) > td:nth-child(3) > a', + ) + .map((i, el) => loadedCheerio(el).text()) + .get() + .join(','), + }; + + const summary = loadedCheerio('#tab1 > div.well'); + // remove h3 and div from children + summary.children().remove('h3, div'); + novel.summary = summary.text().trim(); + + const chapters: Plugin.ChapterItem[] = []; + + // path = manga-the-last-adventurer.html + // title = the-last-adventurer + const title = novelPath.split('.html')[0].slice(6); + + const url = `${this.site}cek/fetch_pages_manga.php?manga_cek=${title}`; + + // make post request to url with form data page=1 + const response = await fetchApi(url, { + ...this.opts, + body: 'page=1', + }); + + const page1 = parseHTML(await response.text()); + + const firstPage = 1; + const lastPage = parseInt( + page1('a[title="Last"]').first().attr('data-page') ?? '1', + ); + + let pageDatas = await Promise.all( + Array.from({ length: lastPage - firstPage }, (_, i) => { + return fetchApi(url, { + ...this.opts, + body: `page=${firstPage + i + 1}`, + }).then(r => r.text()); + }), + ).then(pages => pages.map(p => parseHTML(p))); + + pageDatas = [page1, ...pageDatas]; + + // Most titles have the year number in them (e.g. (2021)) and chapter titles do not, so remove + const novelTitle = novel.name + .toLocaleLowerCase() + .replace(/\([0-9]+\)/g, '') + .trim(); + + // Go through each page and get the chapters + for (const page of pageDatas) { + // For each chapter + page('body > ul > table > tbody > tr').each((_, el) => { + const chap = page(el); + + const chapTitle1 = chap.find('td:nth-child(1) > a').text(); + const updatedChapTitle1 = chapTitle1 + .toLocaleLowerCase() + .replace(novelTitle, 'Ch') + .trim(); + const chapTitle2 = chap.find('td:nth-child(1) > div').text(); + + const chapPath = chap.find('td:nth-child(1) > a').attr('href') ?? ''; + + if (chapPath === '') return; + + chapters.push({ + name: + chapTitle2 !== '' + ? `${updatedChapTitle1}: ${chapTitle2}` + : updatedChapTitle1, + path: chapPath, + chapterNumber: parseFloat(chapTitle1.replace(/[^0-9.]/g, '')), + }); + }); + } + + if (chapters.length > 0) { + novel.chapters = chapters.reverse(); + } + return novel; + } + + popularNovels( + pageNo: number, + { showLatestNovels, filters }: Plugin.PopularNovelsOptions<Filters>, + ): Promise<Plugin.NovelItem[]> { + const params = new URLSearchParams(); + params.append('page', pageNo.toString()); + if (showLatestNovels == true) { + params.append('sort', 'last_update'); + params.append('sort_type', 'DESC'); + } else { + params.append('durum', filters.status.value.toString()); // status + params.append('ceviri', ''); // translation status + params.append('yas', filters.age.value.toString()); // age + params.append('icerik', '2'); // content type -> always novel (2) + params.append('tur', filters.genre.value.toString()); // genre (only 1) + params.append('sort', filters.sort.value.toString()); + params.append('sort_type', filters.sort_type.value.toString()); + } + + const url = `${this.site}manga-list-sayfala.html?${params.toString()}`; + + const parseNovels = (loadedCheerio: CheerioAPI): Plugin.NovelItem[] => { + return loadedCheerio( + '#myCarousel > div.container > div:nth-child(3) > div.col-lg-9.col-md-8 > div.col-md-12', + ) + .map((_, el) => { + const novel = loadedCheerio(el); + return { + name: novel.find('#tables').text(), + path: novel.find('#tables > a').attr('href') ?? '', + cover: novel.find('img.img-thumb').first().attr('src') ?? '', + }; + }) + .toArray(); + }; + + return fetchApi(url) + .then(r => r.text()) + .then(body => { + const loadedCheerio = parseHTML(body); + return parseNovels(loadedCheerio); + }); + } + + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const content = loadedCheerio('#well'); + + return content.html() ?? ''; + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + // Not paginated, so we should paginate results ourselves to prevent overloading + const ITEMS_PER_PAGE = 50; + + const params = new URLSearchParams(); + params.append('icerik', searchTerm); + + const url = `${this.site}arama.html?${params.toString()}`; + + const parseNovels = async ( + loadedCheerio: CheerioAPI, + ): Promise<Plugin.NovelItem[]> => { + const novels: Plugin.NovelItem[] = []; + + let curr = 0; + + for (const el of loadedCheerio('div.char > a + span').toArray()) { + // Done! + if (novels.length === ITEMS_PER_PAGE) break; + // Skip if not a novel + if ( + loadedCheerio(el).text().trim().toLowerCase() != 'novel' && + loadedCheerio(el).next().text().trim().toLowerCase() != 'novel' + ) { + continue; + } + // Skip for pagination + if ((pageNo - 1) * ITEMS_PER_PAGE > curr) { + curr++; + continue; + } + + const novelCheerio = loadedCheerio(el).prev(); + + const mangaSlug = novelCheerio.attr('manga-slug') ?? ''; + + novels.push({ + name: novelCheerio.text(), + path: novelCheerio.attr('href') ?? '', + cover: mangaSlug, // NOTE: This slug gets replaced with the actual cover image later (see below) + }); + } + + // Get all cover images in parallel + return await Promise.all( + novels.map(async novel => { + const url = `${this.site}app/manga/controllers/cont.pop.php`; + const response = await fetchApi(url, { + ...this.opts, + body: `slug=${novel.cover}`, + }); + const body = await response.text(); + const imgCheerio = parseHTML(body); + const img = imgCheerio('img').first().attr('src'); + + novel.cover = img; + return novel; + }), + ); + }; + + return fetchApi(url) + .then(r => r.text()) + .then(body => { + const loadedCheerio = parseHTML(body); + return parseNovels(loadedCheerio); + }); + } + + // resolveUrl(path: string, isNovel?: boolean): string { + // return this.site + path; + // } + + filters = { + sort: { + value: 'views', + label: 'Sırala', + options: [ + { label: 'Adı', value: 'name' }, + { label: 'Popülarite', value: 'views' }, + { label: 'Son Güncelleme', value: 'last_update' }, + ], + type: FilterTypes.Picker, + }, + sort_type: { + value: 'DESC', + label: 'Sırala Türü', + options: [ + { label: 'ASC', value: 'ASC' }, + { label: 'DESC', value: 'DESC' }, + ], + type: FilterTypes.Picker, + }, + status: { + value: '', + label: 'Durum', + options: [ + { label: 'Hepsi', value: '' }, + { label: 'Yayınlanması Tamamlanan', value: '1' }, + { label: 'Devam Eden', value: '2' }, + ], + type: FilterTypes.Picker, + }, + translation: { + value: '', + label: 'Çeviri Durumu', + options: [ + { label: 'Hepsi', value: '' }, + { label: 'Çevirisi Tamamlanan', value: '1' }, + { label: 'Devam Eden', value: '4' }, + { label: 'Bırakılan', value: '2' }, + { label: 'Olmayan', value: '3' }, + ], + type: FilterTypes.Picker, + }, + age: { + value: '', + label: 'Yas', + options: [ + { label: 'Hepsi', value: '' }, + { label: '+16', value: '16' }, + { label: '+18', value: '18' }, + ], + type: FilterTypes.Picker, + }, + genre: { + value: '', + label: 'Tür', + options: [ + { label: 'Hepsi', value: '' }, + { label: '4 Koma', value: '4_Koma' }, + { label: 'Action', value: 'Action' }, + { label: 'Adventure', value: 'Adventure' }, + { label: 'Aliens', value: 'Aliens' }, + { label: 'Art', value: 'Art' }, + { label: 'Biography', value: 'Biography' }, + { label: 'Bishoujo', value: 'Bishoujo' }, + { label: 'Bishounen', value: 'Bishounen' }, + { label: 'Comedy', value: 'Comedy' }, + { label: 'Crime', value: 'Crime' }, + { label: 'Demons', value: 'Demons' }, + { label: 'Doujinshi', value: 'Doujinshi' }, + { label: 'Drama', value: 'Drama' }, + { label: 'Ecchi', value: 'Ecchi' }, + { label: 'Fantasy', value: 'Fantasy' }, + { label: 'Gore', value: 'Gore' }, + { label: 'Harem', value: 'Harem' }, + { label: 'History', value: 'History' }, + { label: 'Horror', value: 'Horror' }, + { label: 'Isekai', value: 'Isekai' }, + { label: 'Josei', value: 'Josei' }, + { label: 'Magic', value: 'Magic' }, + { label: 'Manhua', value: 'Manhua' }, + { label: 'Manhwa', value: 'Manhwa' }, + { label: 'Martial', value: 'Martial' }, + { label: 'Mecha', value: 'Mecha' }, + { label: 'Military', value: 'Military' }, + { label: 'Miscellaneous', value: 'Miscellaneous' }, + { label: 'Monster', value: 'Monster' }, + { label: 'Musical', value: 'Musical' }, + { label: 'Mystery', value: 'Mystery' }, + { label: 'Novel', value: 'Novel' }, + { label: 'Nudity', value: 'Nudity' }, + { label: 'One Shot', value: 'One_Shot' }, + { label: 'Romance', value: 'Romance' }, + { label: 'Psychological', value: 'Psychological' }, + { label: 'Seinen', value: 'Seinen' }, + { label: 'School', value: 'School' }, + { label: 'Sci fi', value: 'Sci_fi' }, + { label: 'Short', value: 'Short' }, + { label: 'Shoujo', value: 'Shoujo' }, + { label: 'Shoujo Ai', value: 'Shoujo_Ai' }, + { label: 'Shounen', value: 'Shounen' }, + { label: 'Shounen Ai', value: 'Shounen_Ai' }, + { label: 'Slice of life', value: 'Slice of life' }, + { label: 'Supernatural', value: 'Supernatural' }, + { label: 'Space', value: 'Space' }, + { label: 'Sports', value: 'Sports' }, + { label: 'Thriller', value: 'Thriller' }, + { label: 'Tragedy', value: 'Tragedy' }, + { label: 'Türkçe Novel', value: 'Türkçe Novel' }, + { label: 'Vampires', value: 'Vampires' }, + { label: 'Violence', value: 'Violence' }, + { label: 'War', value: 'War' }, + { label: 'Webtoon', value: 'Webtoon' }, + { label: 'Western', value: 'Western' }, + ], + type: FilterTypes.Picker, + }, + } satisfies Filters; +} + +export default new MangaTR(); diff --git a/plugins/turkish/epiknovel.ts b/plugins/turkish/epiknovel.ts new file mode 100644 index 000000000..d230e4dc1 --- /dev/null +++ b/plugins/turkish/epiknovel.ts @@ -0,0 +1,160 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +class EpikNovel implements Plugin.PluginBase { + id = 'epiknovel'; + name = 'EpikNovel'; + icon = 'src/tr/epiknovel/icon.png'; + site = 'https://www.epiknovel.com/'; + version = '1.0.0'; + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = this.site + 'seri-listesi?Sayfa=' + pageNo; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.col-lg-12.col-md-12').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3').text(); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele).find('h3 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + // console.log(novels); + + return novels; + } + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const url = this.site + novelPath; + // console.log(url); + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: loadedCheerio('h1#tables').text().trim(), + }; + + novel.cover = loadedCheerio('img.manga-cover').attr('src'); + + novel.summary = loadedCheerio( + '#wrapper > div.row > div.col-md-9 > div:nth-child(6) > p:nth-child(3)', + ) + .text() + .trim(); + + novel.status = loadedCheerio( + '#wrapper > div.row > div.col-md-9 > div.row > div.col-md-9 > h4:nth-child(3) > a', + ) + .text() + .trim(); + + novel.author = loadedCheerio('#NovelInfo > p:nth-child(4)') + .text() + .replace(/Publisher:|\s/g, '') + .trim(); + + const novelChapters: Plugin.ChapterItem[] = []; + + loadedCheerio('table').find('tr').first().remove(); + + loadedCheerio('table') + .find('tr') + .each((idx, ele) => { + const releaseDate = loadedCheerio(ele).find('td:nth-child(3)').text(); + + let chapterName = loadedCheerio(ele).find('td:nth-child(1) > a').text(); + + if (loadedCheerio(ele).find('td:nth-child(1) > span').length > 0) { + chapterName = '🔒 ' + chapterName; + } + + const chapterUrl = loadedCheerio(ele) + .find(' td:nth-child(1) > a') + .attr('href'); + + if (!chapterUrl) return; + + novelChapters.push({ + name: chapterName, + path: chapterUrl.replace(this.site, ''), + releaseTime: releaseDate, + }); + }); + + novel.chapters = novelChapters; + // console.log(novel); + + return novel; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + + // console.log(url); + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + let chapterText = ''; + + if (result.url === this.site + 'login') { + chapterText = 'Premium Chapter'; + } else { + chapterText = loadedCheerio('div#icerik').html() || ''; + } + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = this.site + 'seri-listesi?q=' + searchTerm + '&Sayfa=' + pageNo; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('div.col-lg-12.col-md-12').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3').text(); + const novelCover = loadedCheerio(ele).find('img').attr('data-src'); + const novelUrl = loadedCheerio(ele).find('h3 > a').attr('href'); + + if (!novelUrl) return; + + const novel = { + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }; + + novels.push(novel); + }); + + return novels; + } +} + +export default new EpikNovel(); diff --git a/plugins/ukrainian/bakainua.ts b/plugins/ukrainian/bakainua.ts new file mode 100644 index 000000000..e662d2e92 --- /dev/null +++ b/plugins/ukrainian/bakainua.ts @@ -0,0 +1,285 @@ +import { load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { Filters, FilterTypes } from '@libs/filterInputs'; + +class BakaInUa implements Plugin.PluginBase { + id = 'bakainua'; + name = 'BakaInUA'; + icon = 'src/uk/bakainua/icon.png'; + site = 'https://baka.in.ua'; + version = '3.1.7'; + + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + const fictionIds: string[] = []; + const url = new URL(this.site + '/fictions/alphabetical'); + + if (pageNo > 1) url.searchParams.append('page', pageNo.toString()); + if (showLatestNovels || (filters && filters.only_new.value)) + url.searchParams.append('only_new', '1'); + if (filters) { + if (filters.longreads.value) url.searchParams.append('longreads', '1'); + if (filters.finished.value) url.searchParams.append('finished', '1'); + if (filters.genre.value !== '') + url.searchParams.append('genre', filters.genre.value); + } + + const result = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + $('[data-fiction-picker-id-param]').each((_, elem) => { + const id = $(elem).attr('data-fiction-picker-id-param'); + if (id) fictionIds.push(id); + }); + + const requests = fictionIds.map( + async (id): Promise<Plugin.NovelItem | null> => { + try { + const res = await fetchApi(`${this.site}/fictions/${id}/details`, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + const detailHtml = await res.text(); + const $d = parseHTML(detailHtml); + const link = $d('a').first(); + + return { + name: $d('h3').text().trim(), + path: link.attr('href')?.replace(this.site + '/', '') || '', + cover: this.site + link.find('img').attr('src'), + }; + } catch (e) { + return null; + } + }, + ); + + const novels = await Promise.all(requests); + return novels.filter((n): n is Plugin.NovelItem => n !== null); + } + + async parseNovel(novelUrl: string): Promise<Plugin.SourceNovel> { + // 1. Спочатку відкриваємо сторінку новели + const result = await fetchApi(this.site + '/' + novelUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // 2. Збираємо доступні переклади + const translators = $('turbo-frame#alternative-tabs form') + .map((_, form) => { + const name = $(form).find('button span').first().text().trim(); + const ids = $(form) + .find('input[name="translator[]"]') + .map((_, input) => $(input).attr('value') || '') + .get(); + return { name, ids }; + }) + .get(); + + // 3. Вибір перекладу (за замовчуванням перший) + const selected = translators[0]; + + // 4. Будуємо URL з параметрами translator[] + const url = new URL(this.site + '/' + novelUrl); + if (selected?.ids?.length) { + selected.ids.forEach(id => url.searchParams.append('translator[]', id)); + } + + // 5. Завантажуємо сторінку вже з вибраним перекладом + const translatedRes = await fetchApi(url.toString(), { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const translatedBody = await translatedRes.text(); + const $$ = parseHTML(translatedBody); + + let cover = $$('meta[property="og:image"]').attr('content') || ''; + if (cover && !cover.startsWith('http')) { + cover = this.site + cover; + } + + const novel: Plugin.SourceNovel = { + path: novelUrl, + name: $$('h1').first().text().trim(), + author: $$('#fictions-author-search').text().trim() || 'Невідомо', + artist: $$('#fictions-author-search').text().trim() || 'Невідомо', + cover, + summary: $$('div.whitespace-pre-line') + .first() + .text() + .replace(/\s+/g, ' ') + .trim(), + genres: $$('div.flex.flex-wrap.gap-2 span') + .map((_, el) => $$(el).text().trim()) + .get() + .join(', '), + }; + + // 7. Статус + const statusText = $$('div.text-sm:contains("Статус")') // Знаходимо підпис "Статус" + .prev('div.text-2xl') // Беремо попередній div з класом text-2xl + .text() + .trim(); + + if (statusText.includes('Заверш')) { + novel.status = NovelStatus.Completed; + } else if (statusText.includes('Видаєт')) { + novel.status = NovelStatus.Ongoing; + } else if (statusText.includes('Покину')) { + novel.status = NovelStatus.OnHiatus; + } else { + novel.status = NovelStatus.Unknown; + } + + // 8. Глави + const chapters: Plugin.ChapterItem[] = []; + $$('li.group a[href*="/chapters/"]').each((_, elem) => { + const href = $$(elem).attr('href') || ''; + chapters.push({ + name: $$(elem).find('span').eq(1).text().trim() || 'Розділ', + path: href.replace(this.site + '/', ''), + chapterNumber: + parseFloat($$(elem).find('span').eq(0).text().replace(',', '.')) || 0, + releaseTime: $$(elem).find('span').eq(2).text().trim(), + }); + }); + + novel.chapters = chapters.reverse(); + + return novel; + } + + async parseChapter(chapterUrl: string): Promise<string> { + const result = await fetchApi(this.site + '/' + chapterUrl, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + + // Baka.in.ua використовує ActionText (Trix), текст зазвичай у .trix-content або .prose + const content = $( + '.trix-content, .prose, article, #chapter-content', + ).first(); + + // Якщо основний селектор порожній, шукаємо прихований текст у data-атрібутах (особливість Hotwire/Turbo) + if (!content.text().trim()) { + const hiddenData = $('[data-chapter-content-value]').attr( + 'data-chapter-content-value', + ); + if (hiddenData) return hiddenData; + } + + content.find('script, style, button, form, .ads, .social-share').remove(); + + let chapterHtml = content.html(); + + // Останній шанс: пошук тексту в JSON всередині скриптів через Regex + if (!chapterHtml || chapterHtml.trim().length < 100) { + const match = body.match(/"content\\":\\"(.*?)\\"/); + if (match && match[1]) { + chapterHtml = match[1] + .replace(/\\n/g, '<br>') + .replace(/\\"/g, '"') + .replace(/\\u003c/g, '<') + .replace(/\\u003e/g, '>'); + } + } + + return ( + chapterHtml || + 'Контент не знайдено. Можливо, потрібна авторизація на сайті.' + ); + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/search?filter=fiction&search[]=${encodeURIComponent(searchTerm)}`; + + const result = await fetchApi(url, { + headers: { 'user-agent': 'Mozilla/5.0' }, + }); + + const body = await result.text(); + const $ = parseHTML(body); + const novels: Plugin.NovelItem[] = []; + + $('turbo-frame#fictions-section a[href^="/fictions/"]').each((_, elem) => { + const link = $(elem); + + const href = link.attr('href') || ''; + if (!href) return; + + // витягуємо id + const id = href.replace(/^\/fictions\//, '').replace(/^\/+/, ''); + + const name = link.find('h3').first().text().trim(); + + const img = link.closest('.group').find('img').attr('src'); + + novels.push({ + path: `/fictions/${id}`, // гарантуємо правильний формат + name, + cover: img ? this.site + img : '', + }); + }); + + return novels; + } + + filters = { + genre: { + type: FilterTypes.Picker, + label: 'Жанр', + value: '', + options: [ + { label: 'Всі жанри', value: '' }, + { label: 'BL', value: '19' }, + { label: 'GL', value: '20' }, + { label: 'Авторське', value: '32' }, + { label: 'Бойовик', value: '2' }, + { label: 'Вуся', value: '16' }, + { label: 'Гарем', value: '5' }, + { label: 'Детектив', value: '22' }, + { label: 'Драма', value: '12' }, + { label: 'Жахи', value: '10' }, + { label: 'Ісекай', value: '13' }, + { label: 'Історичне', value: '15' }, + { label: 'Комедія', value: '11' }, + { label: 'ЛГБТ', value: '3' }, + { label: 'Містика', value: '18' }, + { label: 'Омегаверс', value: '30' }, + { label: 'Повсякденність', value: '17' }, + { label: 'Пригоди', value: '7' }, + { label: 'Психологія', value: '28' }, + { label: 'Романтика', value: '1' }, + { label: 'Спорт', value: '9' }, + { label: 'Сюаньхвань', value: '27' }, + { label: 'Сянься', value: '26' }, + { label: 'Трагедія', value: '24' }, + { label: 'Трилер', value: '21' }, + { label: 'Фантастика', value: '8' }, + { label: 'Фанфік', value: '23' }, + { label: 'Фентезі', value: '4' }, + { label: 'Школа', value: '6' }, + ], + }, + only_new: { type: FilterTypes.Switch, label: 'Новинки', value: false }, + longreads: { type: FilterTypes.Switch, label: 'Довгочити', value: false }, + finished: { type: FilterTypes.Switch, label: 'Завершене', value: false }, + } satisfies Filters; +} + +export default new BakaInUa(); diff --git a/plugins/ukrainian/smakolykytl.ts b/plugins/ukrainian/smakolykytl.ts new file mode 100644 index 000000000..7f153cb2c --- /dev/null +++ b/plugins/ukrainian/smakolykytl.ts @@ -0,0 +1,241 @@ +import { Plugin } from '@/types/plugin'; +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import dayjs from 'dayjs'; + +class Smakolykytl implements Plugin.PluginBase { + id = 'smakolykytl'; + name = 'Смаколики'; + site = 'https://smakolykytl.site/'; + apiSite = 'https://api.smakolykytl.site/api/user'; + version = '1.0.1'; + icon = 'src/uk/smakolykytl/icon.png'; + + async popularNovels( + pageNo: number, + { showLatestNovels }: Plugin.PopularNovelsOptions, + ): Promise<Plugin.NovelItem[]> { + const sort = showLatestNovels ? '/updates' : '/projects'; + + const result = await fetchApi(this.apiSite + sort); + const json = (await result.json()) as response; + + const novels: Plugin.NovelItem[] = []; + (json?.projects || json?.updates)?.forEach(novel => + novels.push({ + name: novel.title, + cover: novel.image.url, + path: 'titles/' + novel.id, + }), + ); + + return novels; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const id = novelPath.split('/').pop(); + const result = await fetchApi(this.apiSite + '/projects/' + id); + const book = (await result.json()) as response; + + const novel: Plugin.SourceNovel = { + path: novelPath, + name: book?.project?.title || '', + cover: book?.project?.image?.url, + summary: book?.project?.description, + author: book?.project?.author, + status: book?.project?.status_translate.includes('Триває') + ? NovelStatus.Ongoing + : NovelStatus.Completed, + }; + const tags = [book?.project?.genres, book?.project?.tags] + .flat() + .map(tags => tags?.title) + .filter(tags => tags); + + if (tags.length) { + novel.genres = tags.join(', '); + } + + const chapters: Plugin.ChapterItem[] = []; + const res = await fetchApi(this.apiSite + '/projects/' + id + '/books'); + const data = (await res.json()) as response; + data?.books?.forEach(volume => + volume?.chapters?.forEach(chapter => + chapters.push({ + name: volume.title + ' ' + chapter.title, + path: 'read/' + chapter.id, + releaseTime: dayjs(chapter.modifiedAt).format('LLL'), + chapterNumber: chapters.length + 1, + }), + ), + ); + + novel.chapters = chapters; + return novel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const id = chapterPath.split('/').pop(); + const result = await fetchApi(this.apiSite + '/chapters/' + id); + const json = (await result.json()) as response; + const chapterRaw: HTML[] = JSON.parse(json?.chapter?.content || '[]'); + + const chapterText = jsonToHtml(chapterRaw); + return chapterText; + } + + async searchNovels(searchTerm: string): Promise<Plugin.NovelItem[]> { + const result = await fetchApi(this.apiSite + '/projects'); + const json = (await result.json()) as response; + const searchTitle = searchTerm.toLowerCase(); + const novels: Plugin.NovelItem[] = []; + + json?.projects + ?.filter( + ({ title, id }) => + title.toLowerCase().includes(searchTitle) || String(id) == searchTerm, + ) + ?.forEach(novel => + novels.push({ + name: novel.title, + cover: novel.image.url, + path: 'titles/' + novel.id, + }), + ); + return novels; + } +} + +export default new Smakolykytl(); + +function jsonToHtml(json: HTML[], html = '') { + json.forEach(element => { + switch (element.type) { + case 'hardBreak': + html += '<br>'; + break; + case 'horizontalRule': + html += '<hr>'; + break; + case 'image': + if (element.attrs) { + const attrs = Object.entries(element.attrs) + .filter(attr => attr?.[1]) + .map(attr => `${attr[0]}="${attr[1]}"`); + html += '<img ' + attrs.join('; ') + '>'; + } + break; + case 'paragraph': + html += + '<p>' + + (element.content ? jsonToHtml(element.content) : '<br>') + + '</p>'; + break; + case 'text': + html += element.text; + break; + default: + html += JSON.stringify(element, null, '\t'); //maybe I missed something. + break; + } + }); + return html; +} + +type response = { + projects?: TopLevelProject[]; + updates?: Update[]; + project?: TopLevelProject; + books?: BookElement[]; + chapter?: TopLevelChapter; +}; + +type BookElement = { + id: number; + rank: number; + title: string; + chapters: PurpleChapter[]; +}; + +type PurpleChapter = { + id: number; + title: string; + rank: string; + modifiedAt: Date; +}; + +type TopLevelChapter = { + id: number; + title: string; + rank: string; + content: string; + modifiedAt: Date; + book: ChapterBook; +}; + +type ChapterBook = { + id: number; + rank: number; + title: string; + chapters: FluffyChapter[]; + project: BookProject; +}; + +type FluffyChapter = { + id: number; + rank: string; +}; + +type BookProject = { + id: number; +}; + +type TopLevelProject = { + id: number; + title: string; + description: string; + author: string; + translator: string; + modifiedAt: Date; + alternatives: string; + release: string; + nation: string; + status: string; + status_translate: string; + image: Image; + tags?: Genre[]; + genres?: Genre[]; +}; + +type Genre = { + id: number; + title: string; +}; + +type Image = { + id: number; + url: string; + name: string; +}; + +type Update = { + id: number; + title: string; + author: string; + translator: string; + modifiedAt: Date; + image: Image; +}; + +type HTML = { + type: string; + content?: HTML[]; + attrs?: Attrs; + text?: string; +}; + +type Attrs = { + src: string; + alt: string | null; + title: string | null; +}; diff --git a/plugins/ukrainian/uaranobeclub.broken.ts b/plugins/ukrainian/uaranobeclub.broken.ts new file mode 100644 index 000000000..df226f9b6 --- /dev/null +++ b/plugins/ukrainian/uaranobeclub.broken.ts @@ -0,0 +1,275 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; + +const UA_RANOBE_ID = 'uaranobeclub' as const; +const UA_RANOBE_URL = 'https://uaranobe.club/' as const; + +type Writing = { + id: string; + title: string; + image: string; + slug: string; + description: string; + updatedAt: string; + episodes: Episode[]; + category: Category; + genres: GenreConnection[]; + __typename: string; + scanlators: { + scanlator: { + scanlatorName: string; + username: string; + episodes: Episode[]; + __typename: string; + }; + __typename: string; + }[]; +}; + +type Episode = { + seqTitle: string; + title: string; + slug: string; + subId: string; + text: string; + __typename: string; +}; + +type Category = { + id: string; + name: string; + __typename: string; +}; + +type GenreConnection = { + genreId: string; + genre: Genre; + __typename: string; +}; + +type Genre = { + id: string; + name: string; + __typename: string; +}; + +type WritingsResponse = { + writingsCount: number; + writings: Writing[]; +}; + +type WritingResponse = { + data: { + writingBySlug: Writing; + }; +}; + +type EpisodeResponse = { + data: { + episodeBySlug: Episode; + }; +}; + +class UaRanobeClubApi { + static async fetch<TData = unknown, TVariables = unknown>( + query: string, + variables: TVariables, + ): Promise<TData> { + const response = await fetchApi(`${UA_RANOBE_URL}graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + }); + + return (await response.json()) as TData; + } + + static async fetchChapter( + slug: string, + writingSlug: string, + ): Promise<string> { + const query = ` + query EpisodeBySlug($slug: String!, $writingSlug: String!) { + episodeBySlug(slug: $slug, writingSlug: $writingSlug) { + text + } + } + `; + + const variables = { + slug, + writingSlug, + } as const; + + const response = await UaRanobeClubApi.fetch< + EpisodeResponse, + typeof variables + >(query, variables); + + return response.data.episodeBySlug.text; + } + + static async fetchNovel(slug: string): Promise<Writing> { + const query = ` + query Writing($slug: String!) { + writingBySlug(slug: $slug) { + id + title + originalTitle + image + slug + description + scanlators { + scanlator { + scanlatorName + username + episodes(oldestFirst: false, slug: $slug) { + id + subId + seqTitle + title + slug + __typename + } + __typename + } + __typename + } + genres { + genreId + genre { + id + name + __typename + } + __typename + } + __typename + } + } + `; + + const variables = { slug }; + + const response = await UaRanobeClubApi.fetch< + WritingResponse, + typeof variables + >(query, variables); + + return response?.data?.writingBySlug ?? ''; + } + + static async fetchListWithNovel( + skip = 0, + search = '', + ): Promise<WritingsResponse> { + const query = ` + query Writings($skip: Int!, $search: String) { + writingsCount(search: $search) + writings(skip: $skip, search: $search) { + id + title + image + slug + __typename + } + } + `; + const variables = { + skip, + search, + } as const; + + const response = await UaRanobeClubApi.fetch< + { data: WritingsResponse }, + typeof variables + >(query, variables); + + return response.data; + } +} + +class UaRanobeClub implements Plugin.PluginBase { + id = UA_RANOBE_ID; + name = 'UA Ranobe Club'; + site = UA_RANOBE_URL; + version = '1.1.4'; + icon = `src/uk/${this.id}/icon.png`; + + async popularNovels(page: number): Promise<Plugin.NovelItem[]> { + const skip = (page - 1) * 10; + + const data = await UaRanobeClubApi.fetchListWithNovel(skip, ''); + + const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map( + ({ title, image, slug }) => ({ + name: title, + cover: image, + path: slug, + }), + ); + + return novelItems; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const slug = novelPath.split(UA_RANOBE_URL).join(''); + + const data: Writing = await UaRanobeClubApi.fetchNovel(slug); + + const chapters: Plugin.ChapterItem[] = + data.scanlators?.[0]?.scanlator?.episodes?.map( + ({ title, seqTitle, slug: chapterSlug, subId }) => ({ + name: `${seqTitle}. ${title}`, + path: chapterSlug + `#${slug}`, + chapterNumber: parseInt(subId, 10), // Предполагаем, что subId это число + }), + ) || []; + + const sourceNovel: Plugin.SourceNovel = { + genres: data.genres?.map(({ genre }) => genre.name).join(',') || '', + chapters, + name: data.title, + path: data.slug, + summary: data.description, + cover: data.image, + }; + + return sourceNovel; + } + + async parseChapter(chapterPath: string): Promise<string> { + const [slug, writingSlug] = chapterPath + .split(UA_RANOBE_URL) + .join('') + .split('#'); + const chapterText = await UaRanobeClubApi.fetchChapter(slug, writingSlug); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise<Plugin.NovelItem[]> { + const skip = (page - 1) * 10; + + const data = await UaRanobeClubApi.fetchListWithNovel(skip, searchTerm); + const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map( + ({ title, image, slug }) => ({ + name: title, + cover: image, + path: slug, + }), + ); + + return novelItems; + } +} + +export default new UaRanobeClub(); diff --git a/plugins/vietnamese/LNHako.ts b/plugins/vietnamese/LNHako.ts new file mode 100644 index 000000000..042bdcf92 --- /dev/null +++ b/plugins/vietnamese/LNHako.ts @@ -0,0 +1,498 @@ +import { fetchApi } from '@libs/fetch'; +import { Parser } from 'htmlparser2'; +import { HTMLParser2Util, Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; + +enum ParseNovelAction { + Unknown = 'Unknown', + GetName = 'GetName', + GetSummary = 'GetSummary', + GetInfos = 'GetInfos', + GetGenres = 'GetGenres', + GetCover = 'GetCover', + GetVolumes = 'GetVolumes', +} + +class HakoPlugin implements Plugin.PluginBase { + id = 'ln.hako'; + name = 'Hako'; + icon = 'src/vi/hakolightnovel/icon.png'; + site = 'https://ln.hako.vn'; + version = '1.1.0'; + parseNovels(url: string) { + return fetchApi(url) + .then(res => res.text()) + .then(html => { + const novels: Plugin.NovelItem[] = []; + let tempNovel = {} as Plugin.NovelItem; + let isGettingUrl = false; + let isParsingNovel = false; + const parser = new Parser({ + onopentag(name, attribs) { + if (attribs['class']?.includes('thumb-item-flow')) { + isParsingNovel = true; + } + if (isParsingNovel) { + if (attribs['class']?.includes('series-title')) { + isGettingUrl = true; + } + if (attribs['class']?.includes('img-in-ratio')) { + tempNovel.cover = attribs['data-bg']; + } + if (isGettingUrl && name === 'a') { + tempNovel.name = attribs['title']; + tempNovel.path = attribs['href']; + novels.push(tempNovel); + tempNovel = {} as Plugin.NovelItem; // re-assign new reference + isGettingUrl = false; + isParsingNovel = false; + } + } + }, + }); + parser.write(html); + parser.end(); + return novels; + }); + } + popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site + '/danh-sach'; + if (filters) { + if (filters.alphabet.value) { + link += '/' + filters.alphabet.value; + } + const params = new URLSearchParams(); + for (const novelType of filters.type.value) { + params.append(novelType, '1'); + } + for (const status of filters.status.value) { + params.append(status, '1'); + } + params.append('sapxep', filters.sort.value); + link += '?' + params.toString() + '&page=' + pageNo; + } else { + link += '?page=' + pageNo; + } + return this.parseNovels(link); + } + + parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const novel: Plugin.SourceNovel = { + path: novelPath, + name: '', + author: '', + artist: '', + summary: '', + genres: '', + status: '', + }; + const chapters: Plugin.ChapterItem[] = []; + const getNameHandler: HTMLParser2Util.Handler = { + isDone: false, + isStarted: false, + onopentag(name) { + if (name === 'a') { + this.isStarted = true; + } + }, + ontext(data) { + novel.name += data; + }, + onclosetag() { + if (this.isStarted) { + this.isDone = true; + } + }, + }; + const getSummaryHandler: HTMLParser2Util.Handler & { + newLine: boolean; + } = { + newLine: false, + ontext(data) { + if (this.newLine) { + this.newLine = false; + novel.summary += '\n' + data; + } else { + novel.summary += data; + } + }, + onclosetag() { + this.newLine = true; + }, + }; + const getGenresHandler: HTMLParser2Util.Handler = { + ontext(data) { + novel.genres += data; + }, + }; + enum InfoItem { + Author, + Artist, + Status, + Unknown, + } + const getInfosHandler: HTMLParser2Util.Handler & { info: InfoItem } = { + isStarted: false, + info: InfoItem.Unknown, + onopentag(name, attribs) { + if (attribs['class'] === 'info-item') { + switch (this.info) { + case InfoItem.Unknown: + if (!novel.author) { + this.info = InfoItem.Author; + } + break; + case InfoItem.Author: + this.info = InfoItem.Artist; + break; + case InfoItem.Artist: + this.info = InfoItem.Status; + break; + // we dont need the other info (if exist) + case InfoItem.Status: + this.info = InfoItem.Unknown; + break; + default: + break; + } + } + if (name === 'a') { + this.isStarted = true; + } + }, + ontext(data) { + if (this.isStarted) { + switch (this.info) { + case InfoItem.Author: + novel.author += data; + break; + case InfoItem.Artist: + novel.artist += data; + break; + case InfoItem.Status: + novel.status += data; + break; + default: + break; + } + } + }, + onclosetag(name) { + if (this.isStarted) { + this.isStarted = false; + } + if (name === 'a' && this.info === InfoItem.Status) { + this.isDone = true; + } + }, + }; + const getChapterListHandler: HTMLParser2Util.Handler & { + currentVolume: string; + num: number; + part: number; + readingTime: boolean; + tempChapter: Plugin.ChapterItem; + } = { + currentVolume: '', + num: 0, + part: 1, + isStarted: false, + readingTime: false, + tempChapter: {} as Plugin.ChapterItem, + onopentag(name, attribs) { + if (this.isStarted) { + if (name === 'a' && attribs['title'] !== null) { + const chapterName = attribs['title']; + let chapterNumber = Number( + chapterName.match(/Chương\s*(\d+)/i)?.[1], + ); + if (chapterNumber) { + if (this.num === chapterNumber) { + chapterNumber = this.num + this.part / 10; + this.part += 1; + } else { + this.num = chapterNumber; + this.part = 1; + } + } else { + chapterNumber = this.num + this.part / 10; + this.part++; + } + this.tempChapter = { + path: attribs['href'], + name: chapterName, + page: this.currentVolume, + chapterNumber: chapterNumber, + }; + } else if (attribs['class'] === 'chapter-time') { + this.readingTime = true; + } + } + }, + ontext(data) { + if (this.readingTime) { + const chapterTime = data.split('/').map(x => Number(x)); + this.tempChapter.releaseTime = new Date( + chapterTime[2], + chapterTime[1], + chapterTime[0], + ).toISOString(); + chapters.push(this.tempChapter); + this.readingTime = false; + this.tempChapter = {} as Plugin.ChapterItem; + } + }, + onclosetag() { + if (this.readingTime) this.readingTime = false; + }, + }; + const getVolumesHandler: HTMLParser2Util.Handler & { + isParsingChapterList: boolean; + } = { + isStarted: false, + isDone: false, + isParsingChapterList: false, + onopentag(name, attribs) { + if (attribs['class'] === 'sect-title') { + this.isStarted = true; + getChapterListHandler.currentVolume = ''; + } + if (name === 'ul') { + getChapterListHandler.isStarted = true; + getChapterListHandler.num = 0; + getChapterListHandler.part = 1; + } + getChapterListHandler.onopentag?.(name, attribs); + }, + ontext(data) { + if (this.isStarted) { + getChapterListHandler.currentVolume += data.trim(); + } + getChapterListHandler.ontext?.(data); + }, + onclosetag(name, isImplied) { + getChapterListHandler.onclosetag?.(name, isImplied); + this.isStarted = false; + if (name === 'ul') { + getChapterListHandler.isStarted = false; + } + }, + }; + const parseNovelRouter: HTMLParser2Util.HandlerRouter<ParseNovelAction> = { + handlers: { + Unknown: undefined, + GetName: getNameHandler, + GetCover: undefined, + GetSummary: getSummaryHandler, + GetGenres: getGenresHandler, + GetInfos: getInfosHandler, + GetVolumes: getVolumesHandler, + }, + action: ParseNovelAction.Unknown, + onopentag(name, attribs) { + if (attribs['class'] === 'series-name') { + this.action = ParseNovelAction.GetName; + } else if (!novel.cover && attribs['class']?.includes('img-in-ratio')) { + const background = attribs['style']; + if (background) { + novel.cover = background.substring( + background.indexOf('http'), + background.length - 2, + ); + } + } else if (attribs['class'] === 'summary-content') { + this.action = ParseNovelAction.GetSummary; + } else if (attribs['class'] === 'series-gerne-item') { + this.action = ParseNovelAction.GetGenres; + } else if (attribs['class'] === 'info-item') { + this.action = ParseNovelAction.GetInfos; + } else if (attribs['class']?.includes('volume-list')) { + this.action = ParseNovelAction.GetVolumes; + } + }, + onclosetag(name) { + switch (this.action) { + case ParseNovelAction.GetName: + if (this.handlers.GetName?.isDone) { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetSummary: + if (name === 'div') { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetGenres: + this.action = ParseNovelAction.Unknown; + novel.genres += ','; + break; + case ParseNovelAction.GetInfos: + if (this.handlers.GetInfos?.isDone) { + this.action = ParseNovelAction.Unknown; + } + break; + case ParseNovelAction.GetVolumes: + if (this.handlers.GetVolumes?.isDone) { + this.action = ParseNovelAction.Unknown; + } + break; + default: + break; + } + }, + }; + return fetchApi(this.site + novelPath) + .then(res => res.text()) + .then(html => { + const parser = new Parser({ + onopentag(name, attributes) { + parseNovelRouter.onopentag?.(name, attributes); + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.onopentag?.( + name, + attributes, + ); + } + }, + ontext(data) { + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.ontext?.( + data, + ); + } + }, + onclosetag(name, isImplied) { + if (parseNovelRouter.action) { + parseNovelRouter.handlers[parseNovelRouter.action]?.onclosetag?.( + name, + isImplied, + ); + } + parseNovelRouter.onclosetag?.(name, isImplied); + }, + }); + + parser.write(html); + parser.end(); + novel.chapters = chapters; + switch (novel.status?.trim()) { + case 'Đang tiến hành': + novel.status = NovelStatus.Ongoing; + break; + case 'Tạm ngưng': + novel.status = NovelStatus.OnHiatus; + break; + case 'Completed': + novel.status = NovelStatus.Completed; + break; + default: + novel.status = NovelStatus.Unknown; + } + novel.genres = novel.genres?.replace(/,*\s*$/, ''); + novel.name = novel.name.trim(); + novel.summary = novel.summary?.trim(); + return novel; + }); + } + parseChapter(chapterPath: string): Promise<string> { + return fetchApi(this.site + chapterPath) + .then(res => res.text()) + .then( + html => + html.match( + /(<div id="chapter-content".+?>[^]+)<div style="text-align: center;/, + )?.[1] || 'Không tìm thấy nội dung', + ); + } + searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = + this.site + '/tim-kiem?keywords=' + searchTerm + '&page=' + pageNo; + return this.parseNovels(url); + } + imageRequestInit: Plugin.ImageRequestInit = { + headers: { + Referer: this.site, + }, + }; + filters = { + alphabet: { + type: FilterTypes.Picker, + value: '', + label: 'Chữ cái', + options: [ + { label: 'Tất cả', value: '' }, + { label: 'Khác', value: 'khac' }, + { label: 'A', value: 'a' }, + { label: 'B', value: 'b' }, + { label: 'C', value: 'c' }, + { label: 'D', value: 'd' }, + { label: 'E', value: 'e' }, + { label: 'F', value: 'f' }, + { label: 'G', value: 'g' }, + { label: 'H', value: 'h' }, + { label: 'I', value: 'i' }, + { label: 'J', value: 'j' }, + { label: 'K', value: 'k' }, + { label: 'L', value: 'l' }, + { label: 'M', value: 'm' }, + { label: 'N', value: 'n' }, + { label: 'O', value: 'o' }, + { label: 'P', value: 'p' }, + { label: 'Q', value: 'q' }, + { label: 'R', value: 'r' }, + { label: 'S', value: 's' }, + { label: 'T', value: 't' }, + { label: 'U', value: 'u' }, + { label: 'V', value: 'v' }, + { label: 'W', value: 'w' }, + { label: 'X', value: 'x' }, + { label: 'Y', value: 'y' }, + { label: 'Z', value: 'z' }, + ], + }, + type: { + type: FilterTypes.CheckboxGroup, + label: 'Phân loại', + value: [], + options: [ + { label: 'Truyện dịch', value: 'truyendich' }, + { label: 'Truyện sáng tác', value: 'sangtac' }, + { label: 'Convert', value: 'convert' }, + ], + }, + status: { + type: FilterTypes.CheckboxGroup, + label: 'Tình trạng', + value: [], + options: [ + { label: 'Đang tiến hành', value: 'dangtienhanh' }, + { label: 'Tạm ngưng', value: 'tamngung' }, + { label: 'Đã hoàn thành', value: 'hoanthanh' }, + ], + }, + sort: { + type: FilterTypes.Picker, + label: 'Sắp xếp', + value: 'top', + options: [ + { label: 'A-Z', value: 'tentruyen' }, + { label: 'Z-A', value: 'tentruyenza' }, + { label: 'Mới cập nhật', value: 'capnhat' }, + { label: 'Truyện mới', value: 'truyenmoi' }, + { label: 'Theo dõi', value: 'theodoi' }, + { label: 'Top toàn thời gian', value: 'top' }, + { label: 'Top tháng', value: 'topthang' }, + { label: 'Số từ', value: 'sotu' }, + ], + }, + } satisfies Filters; +} + +export default new HakoPlugin(); diff --git a/plugins/vietnamese/Truyenconect.broken.ts b/plugins/vietnamese/Truyenconect.broken.ts new file mode 100644 index 000000000..815ad2ac5 --- /dev/null +++ b/plugins/vietnamese/Truyenconect.broken.ts @@ -0,0 +1,317 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; + +type VolumeData = { + story: string; + navigation: string; + value: string; +}; + +type ChapterSelect = { + chap_selector: string; + eps_selector: boolean | string; +}; + +type VolumeList = { + chapters: Plugin.ChapterItem[]; + volumes?: VolumeData[]; +}; + +class TruyenConect implements Plugin.PagePlugin { + id = 'truyenconect'; + name = 'Truyen Conect'; + icon = 'src/vi/truyenconect/icon.png'; + customJS = 'src/vi/truyenconect/test.js'; + site = 'https://truyenconect.com'; + version = '1.0.0'; + async sleep(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + parseNovels(loadedCheerio: CheerioAPI, selector: string) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio(selector).each((idx, ele) => { + const url = loadedCheerio(ele).find('a').attr('href'); + if (url) { + novels.push({ + name: loadedCheerio(ele).find('img').attr('alt') || '', + path: url.replace(this.site, ''), + cover: loadedCheerio(ele).find('img').attr('data-src'), + }); + } + }); + return novels; + } + async popularNovels( + pageNo: number, + { + filters, + showLatestNovels, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let link = this.site; + let selector = '.c-page__content > .grid9.block .item-thumb.c-image-hover'; + + if (showLatestNovels) { + selector = + '.c-page__content .page-content-listing.item-big_thumbnail .item-thumb.c-image-hover'; + } else if (filters?.category.value) { + link += '/' + filters.category.value; + selector = + 'table.manga-shortcodes.manga-chapters-listing td[width="10%"]'; + if (filters.category.value === 'the-loai') { + selector = '.item-thumb.hover-details.c-image-hover'; + link += '/' + filters.genre.value; + } + link += '?page=' + pageNo; + } + + const body = await fetchApi(link).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio, selector); + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('option').each((idx, ele) => { + let url = ele.attribs['value']; + if (!url) return; + const chapterId = url.match(/\/(\d+)-/)?.[1]; + if (chapterId) { + url = url.replace(chapterId + '-', '') + '-' + chapterId; + } + const num = url.match(/chuong-(\d+)/)?.[1]; + chapters.push({ + path: url.replace(this.site, ''), + name: loadedCheerio(ele).text().trim(), + chapterNumber: Number(num) || undefined, + }); + }); + return chapters.reverse(); + } + parseVolumes(loadedCheerio: CheerioAPI) { + const volumes: VolumeData[] = []; + loadedCheerio('option').each((idx, ele) => { + volumes.push({ + story: ele.attribs['data-story'], + navigation: ele.attribs['data-navigation'], + value: ele.attribs['value'], + }); + }); + return volumes; + } + async getVolumes(firstChapterUrl: string) { + const chapterId = firstChapterUrl.match(/-(\d+)$/)?.[1]; + if (!chapterId) throw new Error('Không tìm thấy chương'); + const url = this.site + '/truyen/get-chap-selector?chap=' + chapterId; + const data: ChapterSelect = await fetchApi(url).then(r => r.json()); + const volumeList: VolumeList = { + chapters: this.parseChapters(parseHTML(data.chap_selector)), + }; + if (data.eps_selector) { + volumeList.volumes = this.parseVolumes( + parseHTML(data.eps_selector as string), + ); + } + return volumeList; + } + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + name: loadedCheerio('.post-title > h1').text().trim(), + path: novelPath, + chapters: [], + totalPages: 1, + }; + + novel.cover = loadedCheerio('.summary_image > a > img').attr('data-src'); + + loadedCheerio('.post-content_item').each(function () { + const detailName = loadedCheerio(this) + .find('.summary-heading > h5') + .text() + .trim(); + const detail = loadedCheerio(this).find('.summary-content').html(); + + if (!detail) return; + + switch (detailName) { + case 'Thể loại': + novel.genres = loadedCheerio(detail) + .children('a') + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + break; + case 'Tác giả': + novel.author = loadedCheerio(detail).text().trim(); + break; + case 'Hoạ sĩ': + novel.artist = loadedCheerio(detail).text().trim(); + break; + case 'Trạng thái': + switch (detail.trim()) { + case 'Full': + novel.status = NovelStatus.Completed; + break; + case 'Tạm ngưng': + novel.status = NovelStatus.OnHiatus; + break; + case 'Đang tiến hành': + novel.status = NovelStatus.Ongoing; + break; + default: + novel.status = NovelStatus.Unknown; + break; + } + } + }); + + loadedCheerio('.description-summary > div.summary__content > div').remove(); + + novel.summary = loadedCheerio('.description-summary > div.summary__content') + .text() + .trim(); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if (!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + if (volumeList.volumes) { + novel.totalPages = volumeList.volumes.length; + novel.chapters = volumeList.chapters; + } else { + novel.totalPages = 1; + novel.chapters = volumeList.chapters; + } + return novel; + } + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = this.site + novelPath; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + const firstChapLink = loadedCheerio('#init-links > a').first().attr('href'); + if (!firstChapLink) throw new Error('Không tìm thấy truyện'); + await this.sleep(1000); + const volumeList = await this.getVolumes(firstChapLink); + const volumeIndex = Number(page) - 1; + if (!volumeList.volumes) throw new Error('Không tìm thấy truyện'); + if (volumeIndex >= volumeList.volumes.length) + throw new Error('Không tìm thấy volume'); + const volume = volumeList.volumes[volumeIndex]; + const query = `dataEpisodes=${volume.value}&datastory=${volume.story}&dataNavigation=${encodeURIComponent(volume.navigation)}`; + const chaptersUrl = `${this.site}/truyen/getreadingchapAction?${query}`; + await this.sleep(1000); + const res: { err: number; html: string } = await fetchApi(chaptersUrl).then( + r => r.json(), + ); + if (res.err) throw new Error(res.html); + return { + chapters: this.parseChapters(parseHTML(res.html)), + }; + } + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + const chapterText = loadedCheerio('.reading-content').html() || ''; + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const url = `${this.site}?key=${searchTerm}&page=${pageNo}`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio('.tab-thumb.c-image-hover > a').each((idx, ele) => { + const novelName = ele.attribs['title']; + const novelCover = loadedCheerio(ele).find('img').first().attr('src'); + const novelUrl = ele.attribs['href']; + + if (!novelUrl) return; + + novels.push({ + name: novelName, + path: novelUrl.replace(this.site, ''), + cover: novelCover, + }); + }); + + return novels; + } + + filters = { + category: { + label: 'Lọc theo', + value: '', + type: FilterTypes.Picker, + options: [ + { label: '', value: '' }, + { label: 'Thể loại', value: 'the-loai' }, + { label: 'Truyện dịch', value: 'truyen-dich' }, + { label: 'Truyện convert', value: 'convert' }, + ], + }, + genre: { + label: 'Thể loại', + value: 'action', + type: FilterTypes.Picker, + options: [ + { label: 'Action', value: 'action' }, + { label: 'Adult', value: 'adult' }, + { label: 'Adventure', value: 'adventure' }, + { label: 'Chinese novel', value: 'chinese-novel' }, + { label: 'Chuyển Sinh', value: 'chuyen-sinh' }, + { label: 'English Novel', value: 'english-novel' }, + { label: 'Harem', value: 'harem' }, + { label: 'Ecchi', value: 'ecchi' }, + { label: 'Fantasy', value: 'fantasy' }, + { label: 'Drama', value: 'drama' }, + { label: 'Game', value: 'game' }, + { label: 'Tiên hiệp', value: 'tien-hiep' }, + { label: 'Kiếm Hiệp', value: 'kiem-hiep' }, + { label: 'Ngôn Tình', value: 'ngon-tinh' }, + { label: 'Isekai', value: 'isekai' }, + { label: 'Lịch Sử', value: 'lich-su' }, + { label: 'Web Novel', value: 'web-novel' }, + { label: 'Xuyên không', value: 'xuyen-khong' }, + { label: 'Trọng sinh', value: 'trong-sinh' }, + { label: 'Trinh thám', value: 'trinh-tham' }, + { label: 'Dị giới', value: 'di-gioi' }, + { label: 'Huyền ảo', value: 'huyen-ao' }, + { label: 'Sắc Hiệp', value: 'sac-hiep' }, + { label: 'Dị năng', value: 'di-nang' }, + { label: 'Linh dị', value: 'linh-di' }, + { label: 'Đô thị', value: 'do-thi' }, + { label: 'Comedy', value: 'comedy' }, + { label: 'School Life', value: 'school-life' }, + { label: 'Romance', value: 'romance' }, + { label: 'Martial-arts', value: 'martial-arts' }, + { label: 'Light Novel', value: 'light-novel' }, + { label: 'Huyền huyễn', value: 'huyen-huyen' }, + { label: 'Kỳ Huyễn', value: 'ky-huyen' }, + { label: 'Khoa Huyễn', value: 'khoa-huyen' }, + { label: 'Võng Du', value: 'vong-du' }, + { label: 'Đồng Nhân', value: 'dong-nhan' }, + ], + }, + } satisfies Filters; +} + +export default new TruyenConect(); diff --git a/plugins/vietnamese/lightnovelvn.ts b/plugins/vietnamese/lightnovelvn.ts new file mode 100644 index 000000000..37a4d07d4 --- /dev/null +++ b/plugins/vietnamese/lightnovelvn.ts @@ -0,0 +1,171 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; + +type SearchedNovel = { + name: string; + slug: string; + coverUrl: string; +}; +type SearchedResult = { + data?: SearchedNovel[]; +}; + +class LightNovelVN implements Plugin.PagePlugin { + id = 'lightnovel.vn'; + name = 'Light Novel VN'; + version = '1.0.0'; + icon = 'src/vi/lightnovelvn/icon.png'; + filters?: Filters | undefined; + site = 'https://lightnovel.vn'; + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/truyen-hot-ds?page=${pageNo}`; + const body = await fetchApi(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novels: Plugin.NovelItem[] = []; + + loadedCheerio(".flex.flex-col[itemtype='https://schema.org/Book']").each( + (idx, ele) => { + const novelName = loadedCheerio(ele) + .find('h3[itemprop="name"] > a') + .text() + .trim(); + const img = loadedCheerio(ele).find('noscript').html(); + const novelCover = img?.match(/srcSet="([^\s]+)/)?.[1]; + const novelUrl = loadedCheerio(ele) + .find('h3[itemprop="name"] > a') + .attr('href'); + if (novelUrl) + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + }, + ); + + return novels; + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.chapter-list > li').each((idx, ele) => { + const chNum = Number(loadedCheerio(ele).find('div').first().text()); + const chapterUrl = loadedCheerio(ele).find('a').attr('href'); + const name = loadedCheerio(ele).find('a').text().trim(); + if (chapterUrl) { + chapters.push({ + path: chapterUrl.replace(this.site, ''), + name, + chapterNumber: chNum, + }); + } + }); + return chapters; + } + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const url = this.site + novelPath; + const body = await fetchApi(url).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + chapters: [], + name: 'Không có tiêu đề', + totalPages: 1, + }; + + novel.name = loadedCheerio('h1[itemprop="name"]').text().trim(); + + novel.cover = loadedCheerio('header div:nth-child(2) img') + .attr('srcset') + ?.split(/\s+/)[0]; + + const genres: string[] = []; + loadedCheerio('a[itemprop="genre"]').each(function () { + genres.push(loadedCheerio(this).text()); + }); + novel.genres = genres.join(','); + + novel.status = loadedCheerio('span.font-bold.text-size22:last').text(); + if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else if (novel.status === 'Hoàn thành') { + novel.status = NovelStatus.Completed; + } else { + novel.status = NovelStatus.Unknown; + } + novel.author = loadedCheerio('a[itemprop="author"] > span').text(); + + novel.summary = loadedCheerio('#bookIntro').text().replace(/\s+/g, ' '); + const delay = (ms: number) => new Promise(res => setTimeout(res, ms)); + await delay(1000); + const chapterListUrl = url + '/danh-sach-chuong'; + + const chapterListBody = await fetchApi(chapterListUrl).then(r => r.text()); + const loadedChapterList = parseHTML(chapterListBody); + novel.chapters = this.parseChapters(loadedChapterList); + loadedChapterList('nav[aria-label="Pagination"] a').each((index, ele) => { + const href = ele.attribs['href']; + if (href) { + const page = Number(href.match(/\?page=(\d+)/)?.[1]); + if (page && page > novel.totalPages) { + novel.totalPages = page; + } + } + }); + return novel; + } + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = `${this.site}${novelPath}/danh-sach-chuong?page=${page}`; + const chapterListBody = await fetchApi(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(chapterListBody)); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise<string> { + const body = await fetchApi(this.site + chapterPath).then(r => r.text()); + + const loadedCheerio = parseHTML(body); + + const chapterText = loadedCheerio('div.chapter-content').html() || ''; + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + if (pageNo > 1) return []; + const url = `${this.site}/api/book-search`; + const formData = new FormData(); + formData.append('keyword', searchTerm); + + const result: SearchedResult = await fetchApi(url, { + method: 'POST', + body: formData, + }).then(r => r.json()); + + const novels: Plugin.NovelItem[] = + result.data?.map(item => { + return { + name: item.name, + path: '/truyen/' + item.slug, + cover: (this.site + item.coverUrl).replace( + 'default.jpg', + '150.jpg?w=256&q=', + ), + }; + }) || []; + return novels; + } +} + +export default new LightNovelVN(); diff --git a/plugins/vietnamese/nettruyen.ts b/plugins/vietnamese/nettruyen.ts new file mode 100644 index 000000000..004309a21 --- /dev/null +++ b/plugins/vietnamese/nettruyen.ts @@ -0,0 +1,117 @@ +import { fetchApi } from '@libs/fetch'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; +import { CheerioAPI, load as parseHTML } from 'cheerio'; + +class Nettruyen implements Plugin.PagePlugin { + id = 'nettruyen'; + name = 'Nettruyen'; + icon = 'src/vi/nettruyen/icon.png'; + site = 'https://nettruyen.com.vn'; + version = '1.0.0'; + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('#listchuong > ul > li > a.thumb').each((idx, ele) => { + const path = ele.attribs['href']; + if (!path) return; + const name = ele.attribs['title']; + const cover = + this.site + '/' + loadedCheerio(ele).find('img').first().attr('src'); + novels.push({ + path: '/' + path, + name, + cover, + }); + }); + return novels; + } + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/xem-nhieu/trang-${pageNo}.html`; + const body = await fetchApi(url).then(r => r.text()); + return this.parseNovels(parseHTML(body)); + } + parseChapters(loadedCheerio: CheerioAPI) { + const chapters: Plugin.ChapterItem[] = []; + loadedCheerio('ul.list-unstyled > li > a').each((idx, ele) => { + const path = ele.attribs['href']; + if (!path) return; + const name = ele.attribs['title']; + chapters.push({ + path: '/' + path, + name, + chapterNumber: Number(name.match(/Chương (\d+)/)?.[1]), + }); + }); + return chapters; + } + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const url = this.site + novelPath; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('.gioithieutruyen.profile-info > h1').text().trim(), + chapters: [], + totalPages: + Number(loadedCheerio('.phantrang > span:nth-child(2)').text()) || 1, + }; + novel.cover = + this.site + '/' + loadedCheerio('.img-thumb > img').attr('src'); + loadedCheerio('.thongtintruyen.note.note-info').each((idx, ele) => { + const strong = loadedCheerio(ele).find('strong').text().trim(); + switch (strong) { + case 'Tác giả': + novel.author = loadedCheerio(ele) + .text() + .replace(/Tác giả\s+\n?:/, ''); + break; + case 'Trạng thái': { + const text = loadedCheerio(ele).text(); + if (text.includes('Đang ra')) { + novel.status = NovelStatus.Ongoing; + } else if (text.includes('Hoàn thành')) { + novel.status = NovelStatus.Completed; + } else { + novel.status = NovelStatus.Unknown; + } + break; + } + case 'Thể loại': + novel.genres = loadedCheerio('a > span') + .toArray() + .map(gEle => loadedCheerio(gEle).text()) + .join(','); + break; + default: + break; + } + }); + novel.summary = loadedCheerio('.gioithieutruyen > .gioithieutruyen') + .text() + .trim(); + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const id = novelPath.match(/-(\d+)\//)?.[1]; + if (!id) throw new Error('Cant parse page'); + const url = `${this.site}/ajax.chuong.php?id=${id}&page=${page}&url=${novelPath.replace(/\//g, '')}&loai=truyendich`; + const body = await fetchApi(url).then(r => r.text()); + const chapters = this.parseChapters(parseHTML(body)); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise<string> { + const url = this.site + chapterPath; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + return loadedCheerio('#noidungchap').html() || ''; + } + async searchNovels(): Promise<Plugin.NovelItem[]> { + throw new Error('Method not implemented.'); + } +} +export default new Nettruyen(); diff --git a/plugins/vietnamese/truyenchu.broken.ts b/plugins/vietnamese/truyenchu.broken.ts new file mode 100644 index 000000000..65851deca --- /dev/null +++ b/plugins/vietnamese/truyenchu.broken.ts @@ -0,0 +1,152 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; + +class TruyenFull implements Plugin.PagePlugin { + id = 'truyenchu'; + name = 'Truyện Chữ'; + icon = 'src/vi/truyenchu/icon.png'; + site = 'https://truyenchu.vn'; + version = '1.0.0'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('.list-truyen .row').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text(); + + const novelCover = + this.site + + loadedCheerio(ele) + .find("div[data-classname='cover']") + .attr('data-image'); + + const novelUrl = loadedCheerio(ele) + .find('h3.truyen-title > a') + .attr('href'); + if (novelUrl) { + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + } + }); + return novels; + } + parseChapters(html: string) { + const listChapterHTML = html.match( + /("list_chapter":\s?\\{0}"(.+)\\{0}"),/, + )?.[1]; + if (!listChapterHTML) throw new Error('Không tải được chương'); + const listChapter = JSON.parse(`{${listChapterHTML}}`); + const loadedChapterList = parseHTML(listChapter.list_chapter); + const chapters: Plugin.ChapterItem[] = []; + loadedChapterList('ul > li > a').each((idx, ele) => { + const path = ele.attribs['href'].replace(this.site, ''); + if (path) { + chapters.push({ + name: ele.attribs['title'], + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)/)?.[1]), + }); + } + }); + return chapters; + } + async popularNovels(pageNo: number): Promise<Plugin.NovelItem[]> { + const url = `${this.site}/danh-sach/truyen-hot?page=${pageNo}`; + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + + return this.parseNovels(loadedCheerio); + } + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + let lastPage = 1; + const regex = new RegExp(`${novelPath}\\?page=\\d+`, 'g'); + body.match(regex)?.forEach(v => { + const page = Number(v.match(/\?page=(\d+)/)?.[1]); + if (page && page > lastPage) { + lastPage = page; + } + }); + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề', + chapters: [], + totalPages: lastPage, + }; + + novel.cover = this.site + loadedCheerio('div.book > img').attr('src'); + + novel.summary = loadedCheerio('div.desc-text').text().trim(); + + novel.author = loadedCheerio('h3:contains("Tác giả:")') + .parent() + .contents() + .text() + .replace('Tác giả:', ''); + + novel.genres = loadedCheerio('h3:contains("Thể loại")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); + if (novel.status === 'Full') { + novel.status = NovelStatus.Completed; + } else if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else { + novel.status = NovelStatus.Unknown; + } + novel.chapters = this.parseChapters(body); + return novel; + } + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = `${this.site}${novelPath}?page=${page}`; + const result = await fetchApi(url); + const body = await result.text(); + + const chapters = this.parseChapters(body); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = + (loadedCheerio('.chapter-title').html() || '') + + (loadedCheerio('#chapter-c').html() || ''); + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; + + const result = await fetchApi(searchUrl); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } +} + +export default new TruyenFull(); diff --git a/plugins/vietnamese/truyenfull.broken.ts b/plugins/vietnamese/truyenfull.broken.ts new file mode 100644 index 000000000..0c88c5a9d --- /dev/null +++ b/plugins/vietnamese/truyenfull.broken.ts @@ -0,0 +1,186 @@ +import { CheerioAPI, load as parseHTML } from 'cheerio'; +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@/types/plugin'; +import { NovelStatus } from '@libs/novelStatus'; +import { FilterTypes, Filters } from '@libs/filterInputs'; + +class TruyenFull implements Plugin.PagePlugin { + id = 'truyenfull'; + name = 'Truyện Full'; + icon = 'src/vi/truyenfull/icon.png'; + site = 'https://truyenfull.io'; + version = '1.0.2'; + + parseNovels(loadedCheerio: CheerioAPI) { + const novels: Plugin.NovelItem[] = []; + loadedCheerio('.list-truyen .row').each((idx, ele) => { + const novelName = loadedCheerio(ele).find('h3.truyen-title > a').text(); + + const novelCover = loadedCheerio(ele) + .find("div[data-classname='cover']") + .attr('data-image'); + + const novelUrl = loadedCheerio(ele) + .find('h3.truyen-title > a') + .attr('href'); + if (novelUrl) { + novels.push({ + name: novelName, + cover: novelCover, + path: novelUrl.replace(this.site, ''), + }); + } + }); + return novels; + } + parseChapters(loadedCheerio: CheerioAPI): Plugin.ChapterItem[] { + return loadedCheerio('ul.list-chapter > li > a') + .toArray() + .map(ele => { + const path = ele.attribs['href'].replace(this.site, ''); + return { + name: loadedCheerio(ele).text().trim(), + path, + chapterNumber: Number(path.match(/\/chuong-(\d+)\//)?.[1]), + }; + }); + } + async popularNovels( + pageNo: number, + { filters }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + let url = this.site + '/danh-sach'; + + if (filters) { + if (filters.sort.value !== '') { + url += `/${filters.sort.value}`; + } else { + url += `/truyen-hot`; + } + for (const status of filters.status.value) { + url += `/${status}`; + } + } + url += `/trang-${pageNo}`; + + const result = await fetchApi(url); + const body = await result.text(); + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + async parseNovel( + novelPath: string, + ): Promise<Plugin.SourceNovel & { totalPages: number }> { + const url = this.site + novelPath; + + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + let lastPage = 1; + loadedCheerio('ul.pagination.pagination-sm > li > a').each(function () { + const page = Number(this.attribs['href'].match(/\/trang-(\d+)\//)?.[1]); + if (page && page > lastPage) lastPage = page; + }); + + const novel: Plugin.SourceNovel & { totalPages: number } = { + path: novelPath, + name: loadedCheerio('div.book > img').attr('alt') || 'Không có tiêu đề', + chapters: [], + totalPages: lastPage, + }; + + novel.cover = loadedCheerio('div.book > img').attr('src'); + + novel.summary = loadedCheerio('div.desc-text').text().trim(); + + novel.author = loadedCheerio('h3:contains("Tác giả:")') + .parent() + .contents() + .text() + .replace('Tác giả:', ''); + + novel.genres = loadedCheerio('h3:contains("Thể loại")') + .siblings() + .map((i, el) => loadedCheerio(el).text()) + .toArray() + .join(','); + + novel.status = loadedCheerio('h3:contains("Trạng thái")').next().text(); + if (novel.status === 'Full') { + novel.status = NovelStatus.Completed; + } else if (novel.status === 'Đang ra') { + novel.status = NovelStatus.Ongoing; + } else { + novel.status = NovelStatus.Unknown; + } + novel.chapters = this.parseChapters(loadedCheerio); + return novel; + } + async parsePage(novelPath: string, page: string): Promise<Plugin.SourcePage> { + const url = `${this.site}${novelPath}trang-${page}/#list-chapter`; + const result = await fetchApi(url); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + const chapters = this.parseChapters(loadedCheerio); + return { + chapters, + }; + } + async parseChapter(chapterPath: string): Promise<string> { + const result = await fetchApi(this.site + chapterPath); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + + const chapterText = + (loadedCheerio('.chapter-title').html() || '') + + (loadedCheerio('#chapter-c').html() || ''); + + return chapterText; + } + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const searchUrl = `${this.site}/tim-kiem?tukhoa=${searchTerm}&page=${pageNo}`; + + const result = await fetchApi(searchUrl); + const body = await result.text(); + + const loadedCheerio = parseHTML(body); + return this.parseNovels(loadedCheerio); + } + filters = { + status: { + type: FilterTypes.CheckboxGroup, + label: 'Tình trạng', + value: [], + options: [{ label: 'Đã hoàn thành', value: 'hoan' }], + }, + sort: { + type: FilterTypes.Picker, + label: 'Sắp xếp', + value: '', + options: [ + { label: 'Truyện mới cập nhật', value: 'truyen-moi' }, + { label: 'Truyện hot', value: 'truyen-hot' }, + { label: 'Truyện full', value: 'truyen-full' }, + { label: 'Tiên hiệp hay', value: 'tien-hiep-hay' }, + { label: 'Kiếm hiệp hay', value: 'kiem-hiep-hay' }, + { label: 'Truyện teen hay', value: 'truyen-teen-hay' }, + { label: 'Ngôn tình hay', value: 'ngon-tinh-hay' }, + { label: 'Ngôn tình ngược', value: 'ngon-tinh-nguoc' }, + { label: 'Ngôn tình sủng', value: 'ngon-tinh-sung' }, + { label: 'Ngôn tình hài', value: 'ngon-tinh-hai' }, + { label: 'Đam mỹ hay', value: 'dam-my-hay' }, + { label: 'Đam mỹ hài', value: 'dam-my-hai' }, + { label: 'Đam mỹ h văn', value: 'dam-my-h-van' }, + { label: 'Đam mỹ sắc', value: 'dam-my-sac' }, + ], + }, + } satisfies Filters; +} + +export default new TruyenFull(); diff --git a/plugins/vietnamese/truyenss.ts b/plugins/vietnamese/truyenss.ts new file mode 100644 index 000000000..e9415ff75 --- /dev/null +++ b/plugins/vietnamese/truyenss.ts @@ -0,0 +1,309 @@ +import { CheerioAPI, load as parseHTML, type Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; +import { fetchApi } from '@libs/fetch'; +import { FilterTypes, Filters } from '@libs/filterInputs'; +import { NovelStatus } from '@libs/novelStatus'; +import { Plugin } from '@/types/plugin'; + +const CHAPTER_PATH = /^\/truyen\/([^/]+)\/chuong-(\d+)$/; + +class TruyenSS implements Plugin.PluginBase { + id = 'truyenss.com'; + name = 'TruyenSS'; + icon = 'src/vi/truyenss/icon.png'; + site = 'https://truyenss.com'; + version = '1.0.0'; + + imageRequestInit: Plugin.ImageRequestInit = { + headers: { Referer: this.site + '/' }, + }; + + filters = { + genre: { + type: FilterTypes.Picker, + label: 'Thể loại', + value: 'tien-hiep', + options: [ + { label: 'Tiên Hiệp', value: 'tien-hiep' }, + { label: 'Nữ Cường', value: 'nu-cuong' }, + { label: 'Xuyên Không', value: 'xuyen-khong' }, + { label: 'Điền Văn', value: 'dien-van' }, + { label: 'Thám Hiểm', value: 'tham-hiem' }, + { label: 'Linh Dị', value: 'linh-di' }, + { label: 'Truyện Ngược', value: 'truyen-nguoc' }, + { label: 'Truyện Sủng', value: 'truyen-sung' }, + { label: 'Đông Phương', value: 'dong-phuong' }, + { label: 'Hài Hước', value: 'hai-huoc' }, + { label: 'Hiện Đại', value: 'hien-dai' }, + { label: 'Quân Sự', value: 'quan-su' }, + { label: 'Mạt Thế', value: 'mat-the' }, + { label: 'Trọng Sinh', value: 'trong-sinh' }, + { label: 'Đồng Nhân', value: 'dong-nhan' }, + { label: 'Quan Trường', value: 'quan-truong' }, + { label: 'Cổ Đại', value: 'co-dai' }, + { label: 'Hệ Thống', value: 'he-thong' }, + { label: 'Phương Tây', value: 'phuong-tay' }, + { label: 'Lịch Sử', value: 'lich-su' }, + { label: 'Ngôn Tình', value: 'ngon-tinh' }, + { label: 'Huyền Huyễn', value: 'huyen-huyen' }, + { label: 'Kiếm Hiệp', value: 'kiem-hiep' }, + { label: 'Võng Du', value: 'vong-du' }, + { label: 'Trinh Thám', value: 'trinh-tham' }, + { label: 'Khoa Huyễn', value: 'khoa-huyen' }, + { label: 'Dị Năng', value: 'di-nang' }, + { label: 'Gia Đấu Cung Đấu', value: 'gia-dau-cung-dau' }, + { label: 'Góc Nhìn Nữ', value: 'goc-nhin-nu' }, + { label: 'Góc Nhìn Nam', value: 'goc-nhin-nam' }, + ], + }, + } satisfies Filters; + + /** Host-local placeholder from the site (og:image); works with plugin Referer headers. */ + private get sitePlaceholderCover(): string { + return `${this.site}/images/no_avatar.jpg`; + } + + private resolveCoverUrl( + raw: string | undefined, + pageUrl: string, + ): string | undefined { + if (!raw) return undefined; + const u = raw.trim(); + if (!u || u.startsWith('data:')) return undefined; + try { + if (u.startsWith('//')) return 'https:' + u; + if (u.startsWith('http')) return u; + return new URL(u, pageUrl).href; + } catch { + return undefined; + } + } + + private coverFromTruyenAnchor( + loadedCheerio: CheerioAPI, + el: Element, + pageUrl: string, + ): string { + const $a = loadedCheerio(el); + const fromImg = (img: Cheerio<Element>) => { + const src = + img.attr('data-src') || + img.attr('data-lazy-src') || + img.attr('data-original') || + img.attr('src'); + return this.resolveCoverUrl(src, pageUrl); + }; + + const inner = fromImg($a.find('img').first()); + if (inner) return inner; + + const cardImg = $a.closest('.card').find('img').first(); + const fromCard = fromImg(cardImg); + if (fromCard) return fromCard; + + const rowImg = $a.closest('.row').find('img').first(); + const fromRow = fromImg(rowImg); + if (fromRow) return fromRow; + + return this.sitePlaceholderCover; + } + + private collectTruyenLinks( + loadedCheerio: CheerioAPI, + pageUrl: string, + ): Plugin.NovelItem[] { + const novels: Plugin.NovelItem[] = []; + const seen = new Set<string>(); + loadedCheerio('a[href^="/truyen/"]').each((_, el) => { + const href = el.attribs['href']; + if (!href || href.split('/').length !== 3) return; + const path = href.split('?')[0]!; + if (seen.has(path)) return; + seen.add(path); + const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); + if (!name) return; + const cover = this.coverFromTruyenAnchor(loadedCheerio, el, pageUrl); + novels.push({ path, name, cover }); + }); + return novels; + } + + async popularNovels( + pageNo: number, + { + showLatestNovels, + filters, + }: Plugin.PopularNovelsOptions<typeof this.filters>, + ): Promise<Plugin.NovelItem[]> { + if (showLatestNovels) { + if (pageNo > 1) return []; + const body = await fetchApi(this.site + '/').then(r => r.text()); + return this.collectTruyenLinks(parseHTML(body), `${this.site}/`); + } + const genre = filters?.genre.value ?? 'tien-hiep'; + const url = + pageNo <= 1 + ? `${this.site}/${genre}` + : `${this.site}/${genre}?page=${pageNo}`; + const body = await fetchApi(url).then(r => r.text()); + return this.collectTruyenLinks(parseHTML(body), url); + } + + private parseStatusLine(raw: string): string { + const t = raw.toLowerCase(); + if (t.includes('hoàn') || t.includes('full')) return NovelStatus.Completed; + if (t.includes('đang') || t.includes('ra chương')) + return NovelStatus.Ongoing; + return NovelStatus.Unknown; + } + + private parseChapters( + loadedCheerio: CheerioAPI, + novelPath: string, + ): Plugin.ChapterItem[] { + const chapters: Plugin.ChapterItem[] = []; + const h2 = loadedCheerio('h2') + .filter((_, el) => loadedCheerio(el).text().includes('Danh Sách Chương')) + .first(); + const container = h2.next('div.position-relative'); + const anchors = container.length + ? container.find('a[href^="#"]') + : loadedCheerio('#inner-page a[href^="#"]'); + + anchors.each((_, el) => { + const href = el.attribs['href']; + if (!href?.startsWith('#')) return; + const num = Number(href.slice(1)); + if (!Number.isFinite(num) || num <= 0) return; + const name = loadedCheerio(el).text().replace(/\s+/g, ' ').trim(); + chapters.push({ + name: name || `Chương ${num}`, + path: `${novelPath}/chuong-${num}`, + chapterNumber: num, + }); + }); + chapters.sort((a, b) => (a.chapterNumber ?? 0) - (b.chapterNumber ?? 0)); + return chapters; + } + + async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> { + const path = novelPath; + const url = this.site + path; + const body = await fetchApi(url).then(r => r.text()); + const loadedCheerio = parseHTML(body); + + const novel: Plugin.SourceNovel = { + path, + name: + loadedCheerio('#inner-page > h1').first().text().trim() || + loadedCheerio('main#main h1').first().text().trim() || + 'Không có tiêu đề', + chapters: [], + }; + + const coverSrc = loadedCheerio('.info_truyen img.avatar').attr('src'); + novel.cover = + this.resolveCoverUrl(coverSrc, url) ?? this.sitePlaceholderCover; + + const infoBlock = loadedCheerio('.info_truyen').first(); + const infoText = infoBlock.text(); + const authorMatch = infoText.match(/Tác\s*Giả:\s*([^\n\r]+)/i); + if (authorMatch) novel.author = authorMatch[1]!.trim(); + + const statusMatch = infoText.match(/Tình\s*Trạng:\s*([^\n\r]+)/i); + if (statusMatch) novel.status = this.parseStatusLine(statusMatch[1]!); + + novel.genres = loadedCheerio('p.tags a.badge') + .toArray() + .map(a => loadedCheerio(a).text().trim()) + .filter(Boolean) + .join(', '); + + const intro = loadedCheerio( + '#inner-page .position-relative.mt-4 .line-height-3', + ).first(); + if (intro.length) { + const block = intro.clone(); + block.find('script, style').remove(); + block.find('br').replaceWith('\n'); + block.find('p').before('\n').after('\n\n'); + novel.summary = block + .text() + .split('\n') + .map(line => line.replace(/\s+/g, ' ').trim()) + .filter(Boolean) + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); + } + + novel.chapters = this.parseChapters(loadedCheerio, path); + return novel; + } + + private extractChapterBody($: CheerioAPI): string { + $('script, style').remove(); + let best = ''; + let bestP = 0; + $('div').each((_, el) => { + const div = $(el); + const pCount = div.find('p').length; + if (pCount > bestP) { + bestP = pCount; + best = div.html() ?? ''; + } + }); + if (bestP >= 2) return best; + const fallback = $('body').html() ?? $.root().html() ?? ''; + return fallback; + } + + async parseChapter(chapterPath: string): Promise<string> { + let rel = chapterPath; + if (rel.startsWith(this.site)) { + rel = rel.slice(this.site.length); + } + const m = rel.match(CHAPTER_PATH); + if (!m) throw new Error(`TruyenSS: invalid chapter path: ${rel}`); + const folder = m[1]!; + const chuong = m[2]!; + const referer = `${this.site}/truyen/${folder}`; + + const qs = new URLSearchParams({ folder, chuong }).toString(); + const body = await fetchApi(`${this.site}/layout/xem-chuong.php?${qs}`, { + headers: { + 'X-Requested-With': 'XMLHttpRequest', + Referer: referer, + }, + }).then(r => r.text()); + + if (!body.trim()) { + throw new Error('TruyenSS: empty chapter response'); + } + + return this.extractChapterBody(parseHTML(body)); + } + + async searchNovels( + searchTerm: string, + pageNo: number, + ): Promise<Plugin.NovelItem[]> { + const q = encodeURIComponent(searchTerm.trim()); + if (!q) return []; + + const tryUrls = [ + `${this.site}/tim-kiem?q=${q}&page=${pageNo}`, + `${this.site}/tim-kiem/${q}?page=${pageNo}`, + `${this.site}/tim-truyen?tu-khoa=${q}&page=${pageNo}`, + ]; + + for (const tryUrl of tryUrls) { + const body = await fetchApi(tryUrl).then(r => r.text()); + const novels = this.collectTruyenLinks(parseHTML(body), tryUrl); + if (novels.length) return novels; + } + return []; + } +} + +export default new TruyenSS(); diff --git a/proxy.ts b/proxy.ts new file mode 100644 index 000000000..2d0c7277c --- /dev/null +++ b/proxy.ts @@ -0,0 +1,277 @@ +import process from 'node:process'; +import { Buffer } from 'buffer'; +import { FetchMode, ServerSetting } from './src/types/types'; +import { Connect } from 'vite'; +import httpProxy from 'http-proxy'; +import { exec } from 'child_process'; +import { brotliDecompressSync, gunzipSync, zstdDecompressSync } from 'zlib'; + +const proxy = httpProxy.createProxyServer({}); + +const settings: ServerSetting = { + CLIENT_HOST: 'http://localhost:3000', + fetchMode: FetchMode.PROXY, + disAllowedRequestHeaders: [ + 'sec-ch-ua', + 'sec-ch-ua-mobile', + 'sec-ch-ua-platform', + 'sec-fetch-site', + 'origin', + 'sec-fetch-site', + 'sec-fetch-dest', + 'pragma', + ], + disAllowResponseHeaders: [ + 'link', + 'set-cookie', + 'set-cookie2', + 'content-encoding', + 'content-length', + ], + useUserAgent: true, +}; + +const proxySettingMiddleware: Connect.NextHandleFunction = (req, res) => { + if (req.method === 'GET') { + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.write(JSON.stringify(settings)); + res.end(); + return; + } + + let str = ''; + req.on('data', chunk => { + str += chunk; + }); + req.on('end', () => { + try { + const newSettings = JSON.parse(str); + for (const key in newSettings) { + // @ts-ignore + settings[key] = newSettings[key]; + } + res.statusCode = 200; + res.setHeader('Content-Type', 'application/json'); + res.write(JSON.stringify(settings)); + } catch { + res.statusCode = 400; + } finally { + res.end(); + } + }); +}; + +const proxyHandlerMiddle: Connect.NextHandleFunction = (req, res) => { + const rawUrl = 'https:' + req.url; + if (req.headers['access-control-request-method']) { + res.setHeader( + 'access-control-allow-methods', + req.headers['access-control-request-method'], + ); + delete req.headers['access-control-request-method']; + } + if (req.headers['access-control-request-headers']) { + res.setHeader( + 'access-control-allow-headers', + req.headers['access-control-request-headers'], + ); + delete req.headers['access-control-request-headers']; + } + 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(); + } else { + try { + const _url = new URL(rawUrl); + for (const _header in req.headers) { + if ( + req.headers[_header]?.includes('localhost') || + settings.disAllowedRequestHeaders.includes(_header) + ) { + delete req.headers[_header]; + } + } + req.headers['sec-fetch-mode'] = 'cors'; + if (settings.cookies) req.headers['cookie'] = settings.cookies; + if (!settings.useUserAgent) delete req.headers['user-agent']; + req.headers.host = _url.host; + req.url = _url.toString(); + proxyRequest(req, res); + } catch (err) { + console.log('\x1b[31m', '----------ERRROR----------'); + console.error(err); + console.log('\x1b[31m', '----------ERRROR----------'); + if (!res.closed) { + res.statusCode = 500; + res.end(); + } + } + } +}; + +const proxyRequest: Connect.SimpleHandleFunction = (req, res) => { + const _url = new URL(req.url || ''); + console.log('\x1b[36m', '----------------'); + console.log( + `Making proxy request - at ${new Date().toLocaleTimeString()} + url: ${_url.href} + headers:`, + ); + Object.entries(req.headers).forEach(([name, value]) => { + console.log('\t', '\x1b[32m', name + ':', '\x1b[37m', value); + }); + console.log('\x1b[36m', '----------------'); + + if (settings.fetchMode === FetchMode.CURL) { + let curl = `curl -L '${_url.href}'`; + if (settings.useUserAgent) + curl += ` -H 'User-Agent: ${req.headers['user-agent']}'`; + if (settings.cookies) curl += ` -H 'Cookie: ${settings.cookies}'`; + if (req.headers.origin2) curl += ` -H 'Origin: ${req.headers.origin2}'`; + + const isWindows = process.platform === 'win32'; + const options = isWindows + ? { + shell: + process.env.BASH_LOCATION || + process.env.ProgramFiles + '\\git\\usr\\bin\\bash.exe', + } + : {}; + + exec(curl, options, (error, stdout) => { + if (error) { + res.statusCode = 500; + res.write(`exec error: ${error}`); + res.end(); + return; + } + res.statusCode = 200; + res.write(stdout); + res.end(); + }); + } else if (settings.fetchMode === FetchMode.NODE_FETCH) { + const headers = new Headers(); + if (settings.useUserAgent) + headers.append('user-agent', req.headers['user-agent'] as string); + if (settings.cookies) headers.append('cookie', settings.cookies); + if (req.headers.origin2) + headers.append('origin', req.headers.origin2 as string); + + fetch(_url.href, { headers }) + .then(async res2 => { + res.statusCode = res2.status; + res2.headers.forEach((val, key) => { + if (!settings.disAllowResponseHeaders.includes(key)) { + res.setHeader(key, val); + } + }); + res.write(await res2.text()); + res.end(); + }) + .catch(err => { + console.error(err); + res.statusCode = 500; + res.end(); + }); + } else if (settings.fetchMode === FetchMode.PROXY) { + proxy.web( + req, + res, + { target: _url.origin, selfHandleResponse: true, followRedirects: true }, + err => { + console.error('Proxy target error:', err); + res.statusCode = 500; + res.end(); + }, + ); + } +}; + +proxy.on('proxyRes', function (proxyRes, req, res) { + const statusCode = proxyRes.statusCode || 200; + + // Redirect handling + if ([301, 302, 303, 307, 308].includes(statusCode)) { + const location = proxyRes.headers['location']; + if (location) { + try { + const _url = new URL(req.url || ''); + const redirectUrl = new URL(location, _url.href); + req.url = redirectUrl.toString(); + + // Prevent infinite loops + const reqWithRedirect = req as Connect.IncomingMessage & { + _redirectCount?: number; + }; + const redirectCount = reqWithRedirect._redirectCount || 0; + if (redirectCount >= 5) { + res.statusCode = 508; + res.end('Too many redirects'); + return; + } + reqWithRedirect._redirectCount = redirectCount + 1; + + // Update method for 301/302/303 to GET as per spec + if ([301, 302, 303].includes(statusCode)) { + req.method = 'GET'; + req.headers['content-length'] = '0'; + delete req.headers['content-type']; + } + + req.removeAllListeners(); + proxyRequest(req, res); + return; + } catch (err) { + console.error('Redirect parsing error:', err); + } + } + } + + res.statusCode = statusCode; + + // Propagate headers but filter restricted ones + Object.keys(proxyRes.headers).forEach(key => { + if (!settings.disAllowResponseHeaders.includes(key)) { + res.setHeader(key, proxyRes.headers[key] as string); + } + }); + + if (statusCode === 304) { + res.end(); + return; + } + + const contentEncoding = proxyRes.headers['content-encoding'] || ''; + const chunks: Buffer[] = []; + proxyRes.on('data', chunk => chunks.push(Buffer.from(chunk))); + proxyRes.on('end', () => { + try { + const compressedBuffer = Buffer.concat(chunks); + if (compressedBuffer.length > 0) { + let decompressedBuffer: Buffer; + if (contentEncoding.includes('br')) { + decompressedBuffer = brotliDecompressSync(compressedBuffer); + } else if (contentEncoding.includes('gzip')) { + decompressedBuffer = gunzipSync(compressedBuffer); + } else if (contentEncoding.includes('zstd')) { + decompressedBuffer = zstdDecompressSync(compressedBuffer); + } else { + decompressedBuffer = compressedBuffer; + } + res.write(decompressedBuffer); + } + res.end(); + } catch (err) { + console.error('Decompression error:', err); + res.statusCode = 500; + res.end('Decompression error'); + } + }); +}); + +export { proxyHandlerMiddle, proxySettingMiddleware }; diff --git a/public/icon.svg b/public/icon.svg new file mode 100644 index 000000000..c9614f92c --- /dev/null +++ b/public/icon.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none"> + <rect width="64" height="64" rx="12" fill="#E3F1F4" /> + <text + x="50%" + y="50%" + dominant-baseline="central" + text-anchor="middle" + font-family="'Noto Sans JP', 'Hiragino Kaku Gothic Pro', sans-serif" + font-size="34" + fill="#106E81" + font-weight="600" + > + 読 + </text> +</svg> diff --git a/public/static/coverNotAvailable.webp b/public/static/coverNotAvailable.webp new file mode 100644 index 000000000..8608d7722 Binary files /dev/null and b/public/static/coverNotAvailable.webp differ diff --git a/public/static/multisrc/fictioneer/cherrymistcafe/icon.png b/public/static/multisrc/fictioneer/cherrymistcafe/icon.png new file mode 100644 index 000000000..bf7eecd3d Binary files /dev/null and b/public/static/multisrc/fictioneer/cherrymistcafe/icon.png differ diff --git a/public/static/multisrc/fictioneer/daoistquest/icon.png b/public/static/multisrc/fictioneer/daoistquest/icon.png new file mode 100644 index 000000000..6a57c2451 Binary files /dev/null and b/public/static/multisrc/fictioneer/daoistquest/icon.png differ diff --git a/public/static/multisrc/fictioneer/dearestrosalie/icon.png b/public/static/multisrc/fictioneer/dearestrosalie/icon.png new file mode 100644 index 000000000..d57b2d565 Binary files /dev/null and b/public/static/multisrc/fictioneer/dearestrosalie/icon.png differ diff --git a/public/static/multisrc/fictioneer/lilyonthevalley/icon.png b/public/static/multisrc/fictioneer/lilyonthevalley/icon.png new file mode 100644 index 000000000..239255048 Binary files /dev/null and b/public/static/multisrc/fictioneer/lilyonthevalley/icon.png differ diff --git a/public/static/multisrc/fictioneer/novelib/icon.png b/public/static/multisrc/fictioneer/novelib/icon.png new file mode 100644 index 000000000..fc2193bcf Binary files /dev/null and b/public/static/multisrc/fictioneer/novelib/icon.png differ diff --git a/public/static/multisrc/fictioneer/penguinsquad/icon.png b/public/static/multisrc/fictioneer/penguinsquad/icon.png new file mode 100644 index 000000000..abeab8d3b Binary files /dev/null and b/public/static/multisrc/fictioneer/penguinsquad/icon.png differ diff --git a/public/static/multisrc/fictioneer/prizmatranslation/icon.png b/public/static/multisrc/fictioneer/prizmatranslation/icon.png new file mode 100644 index 000000000..958b50632 Binary files /dev/null and b/public/static/multisrc/fictioneer/prizmatranslation/icon.png differ diff --git a/public/static/multisrc/hotnovelpub/eznovels/icon.png b/public/static/multisrc/hotnovelpub/eznovels/icon.png new file mode 100644 index 000000000..b28af5c39 Binary files /dev/null and b/public/static/multisrc/hotnovelpub/eznovels/icon.png differ diff --git a/public/static/multisrc/hotnovelpub/hotnovelpub/icon.png b/public/static/multisrc/hotnovelpub/hotnovelpub/icon.png new file mode 100644 index 000000000..21e56566a Binary files /dev/null and b/public/static/multisrc/hotnovelpub/hotnovelpub/icon.png differ diff --git a/public/static/multisrc/hotnovelpub/lanovels/icon.png b/public/static/multisrc/hotnovelpub/lanovels/icon.png new file mode 100644 index 000000000..c35015230 Binary files /dev/null and b/public/static/multisrc/hotnovelpub/lanovels/icon.png differ diff --git a/public/static/multisrc/hotnovelpub/lightnoveldaily/icon.png b/public/static/multisrc/hotnovelpub/lightnoveldaily/icon.png new file mode 100644 index 000000000..edcb5aa3a Binary files /dev/null and b/public/static/multisrc/hotnovelpub/lightnoveldaily/icon.png differ diff --git a/public/static/multisrc/hotnovelpub/thnovels/icon.png b/public/static/multisrc/hotnovelpub/thnovels/icon.png new file mode 100644 index 000000000..c35015230 Binary files /dev/null and b/public/static/multisrc/hotnovelpub/thnovels/icon.png differ diff --git a/public/static/multisrc/ifreedom/bookhamster/icon.png b/public/static/multisrc/ifreedom/bookhamster/icon.png new file mode 100644 index 000000000..1ce85beae Binary files /dev/null and b/public/static/multisrc/ifreedom/bookhamster/icon.png differ diff --git a/public/static/multisrc/ifreedom/ifreedom/icon.png b/public/static/multisrc/ifreedom/ifreedom/icon.png new file mode 100644 index 000000000..ca3c54726 Binary files /dev/null and b/public/static/multisrc/ifreedom/ifreedom/icon.png differ diff --git a/public/static/multisrc/lightnovelworld/lightnovelcave/icon.png b/public/static/multisrc/lightnovelworld/lightnovelcave/icon.png new file mode 100644 index 000000000..70d4f7271 Binary files /dev/null and b/public/static/multisrc/lightnovelworld/lightnovelcave/icon.png differ diff --git a/public/static/multisrc/lightnovelworld/lightnovelpubvip/icon.png b/public/static/multisrc/lightnovelworld/lightnovelpubvip/icon.png new file mode 100644 index 000000000..728a41b65 Binary files /dev/null and b/public/static/multisrc/lightnovelworld/lightnovelpubvip/icon.png differ diff --git a/public/static/multisrc/lightnovelworld/webnovelworld/icon.png b/public/static/multisrc/lightnovelworld/webnovelworld/icon.png new file mode 100644 index 000000000..728a41b65 Binary files /dev/null and b/public/static/multisrc/lightnovelworld/webnovelworld/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/allnovelread/icon.png b/public/static/multisrc/lightnovelwp/allnovelread/icon.png new file mode 100644 index 000000000..dd0d9d60f Binary files /dev/null and b/public/static/multisrc/lightnovelwp/allnovelread/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/arcane/icon.png b/public/static/multisrc/lightnovelwp/arcane/icon.png new file mode 100644 index 000000000..bc80ecc8d Binary files /dev/null and b/public/static/multisrc/lightnovelwp/arcane/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/bacalightnovel/icon.png b/public/static/multisrc/lightnovelwp/bacalightnovel/icon.png new file mode 100644 index 000000000..f7f4cb6a2 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/bacalightnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/betternovels/icon.png b/public/static/multisrc/lightnovelwp/betternovels/icon.png new file mode 100644 index 000000000..0af64d7f0 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/betternovels/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/centralnovel/icon.png b/public/static/multisrc/lightnovelwp/centralnovel/icon.png new file mode 100644 index 000000000..923580475 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/centralnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/cpunovel/icon.png b/public/static/multisrc/lightnovelwp/cpunovel/icon.png new file mode 100644 index 000000000..9b3bedb0a Binary files /dev/null and b/public/static/multisrc/lightnovelwp/cpunovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/daotranslate/icon.png b/public/static/multisrc/lightnovelwp/daotranslate/icon.png new file mode 100644 index 000000000..9b3bedb0a Binary files /dev/null and b/public/static/multisrc/lightnovelwp/daotranslate/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/ellotl/icon.png b/public/static/multisrc/lightnovelwp/ellotl/icon.png new file mode 100644 index 000000000..93b22401c Binary files /dev/null and b/public/static/multisrc/lightnovelwp/ellotl/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/freekolnovel/icon.png b/public/static/multisrc/lightnovelwp/freekolnovel/icon.png new file mode 100644 index 000000000..c232aac64 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/freekolnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/ippotranslations/icon.png b/public/static/multisrc/lightnovelwp/ippotranslations/icon.png new file mode 100644 index 000000000..32c51b117 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/ippotranslations/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/kdtnovels/icon.png b/public/static/multisrc/lightnovelwp/kdtnovels/icon.png new file mode 100644 index 000000000..10692abb9 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/kdtnovels/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/keopi/icon.png b/public/static/multisrc/lightnovelwp/keopi/icon.png new file mode 100644 index 000000000..caf7a2cb9 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/keopi/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/knoxt/icon.png b/public/static/multisrc/lightnovelwp/knoxt/icon.png new file mode 100644 index 000000000..d6603be5a Binary files /dev/null and b/public/static/multisrc/lightnovelwp/knoxt/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/kodekslibrary/icon.png b/public/static/multisrc/lightnovelwp/kodekslibrary/icon.png new file mode 100644 index 000000000..cedf43f57 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/kodekslibrary/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/kolnovel/icon.png b/public/static/multisrc/lightnovelwp/kolnovel/icon.png new file mode 100644 index 000000000..c232aac64 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/kolnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/lazygirltranslations/icon.png b/public/static/multisrc/lightnovelwp/lazygirltranslations/icon.png new file mode 100644 index 000000000..8a1a6a53e Binary files /dev/null and b/public/static/multisrc/lightnovelwp/lazygirltranslations/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/lightnovelbrasil/icon.png b/public/static/multisrc/lightnovelwp/lightnovelbrasil/icon.png new file mode 100644 index 000000000..dcabe5ba4 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/lightnovelbrasil/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/lightnovelfr/icon.png b/public/static/multisrc/lightnovelwp/lightnovelfr/icon.png new file mode 100644 index 000000000..927e400ff Binary files /dev/null and b/public/static/multisrc/lightnovelwp/lightnovelfr/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/moonlightnovel/icon.png b/public/static/multisrc/lightnovelwp/moonlightnovel/icon.png new file mode 100644 index 000000000..7c8f73228 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/moonlightnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/namevt/icon.png b/public/static/multisrc/lightnovelwp/namevt/icon.png new file mode 100644 index 000000000..e86457f23 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/namevt/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/noblemtl/icon.png b/public/static/multisrc/lightnovelwp/noblemtl/icon.png new file mode 100644 index 000000000..064e38431 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/noblemtl/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/novelsknight/icon.png b/public/static/multisrc/lightnovelwp/novelsknight/icon.png new file mode 100644 index 000000000..089d30975 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/novelsknight/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/novelsparadise/icon.png b/public/static/multisrc/lightnovelwp/novelsparadise/icon.png new file mode 100644 index 000000000..ff40e958e Binary files /dev/null and b/public/static/multisrc/lightnovelwp/novelsparadise/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/noveltr/icon.png b/public/static/multisrc/lightnovelwp/noveltr/icon.png new file mode 100644 index 000000000..80eac7542 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/noveltr/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/pandamtl/icon.png b/public/static/multisrc/lightnovelwp/pandamtl/icon.png new file mode 100644 index 000000000..eb2259fde Binary files /dev/null and b/public/static/multisrc/lightnovelwp/pandamtl/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/requiemtls/icon.png b/public/static/multisrc/lightnovelwp/requiemtls/icon.png new file mode 100644 index 000000000..e981aa3ba Binary files /dev/null and b/public/static/multisrc/lightnovelwp/requiemtls/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/sektenovel/icon.png b/public/static/multisrc/lightnovelwp/sektenovel/icon.png new file mode 100644 index 000000000..5ac1b62b2 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/sektenovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/systemtranslation/icon.png b/public/static/multisrc/lightnovelwp/systemtranslation/icon.png new file mode 100644 index 000000000..ddcda2977 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/systemtranslation/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/tcsega/icon.png b/public/static/multisrc/lightnovelwp/tcsega/icon.png new file mode 100644 index 000000000..b45a6c09b Binary files /dev/null and b/public/static/multisrc/lightnovelwp/tcsega/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/transweaver/icon.png b/public/static/multisrc/lightnovelwp/transweaver/icon.png new file mode 100644 index 000000000..9a74d7fd6 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/transweaver/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/universalnovel/icon.png b/public/static/multisrc/lightnovelwp/universalnovel/icon.png new file mode 100644 index 000000000..6f7460442 Binary files /dev/null and b/public/static/multisrc/lightnovelwp/universalnovel/icon.png differ diff --git a/public/static/multisrc/lightnovelwp/whitemoonlightnovels/icon.png b/public/static/multisrc/lightnovelwp/whitemoonlightnovels/icon.png new file mode 100644 index 000000000..55897c16f Binary files /dev/null and b/public/static/multisrc/lightnovelwp/whitemoonlightnovels/icon.png differ diff --git a/public/static/multisrc/madara/1stkissnovel/icon.png b/public/static/multisrc/madara/1stkissnovel/icon.png new file mode 100644 index 000000000..96706e844 Binary files /dev/null and b/public/static/multisrc/madara/1stkissnovel/icon.png differ diff --git a/public/static/multisrc/madara/arnovel/icon.png b/public/static/multisrc/madara/arnovel/icon.png new file mode 100644 index 000000000..fff16541d Binary files /dev/null and b/public/static/multisrc/madara/arnovel/icon.png differ diff --git a/public/static/multisrc/madara/asuralightnovel/icon.png b/public/static/multisrc/madara/asuralightnovel/icon.png new file mode 100644 index 000000000..b532c7bd3 Binary files /dev/null and b/public/static/multisrc/madara/asuralightnovel/icon.png differ diff --git a/public/static/multisrc/madara/azora/icon.png b/public/static/multisrc/madara/azora/icon.png new file mode 100644 index 000000000..684bef3d2 Binary files /dev/null and b/public/static/multisrc/madara/azora/icon.png differ diff --git a/public/static/multisrc/madara/azraznovel/icon.png b/public/static/multisrc/madara/azraznovel/icon.png new file mode 100644 index 000000000..e485e5702 Binary files /dev/null and b/public/static/multisrc/madara/azraznovel/icon.png differ diff --git a/public/static/multisrc/madara/bellereservoir/icon.png b/public/static/multisrc/madara/bellereservoir/icon.png new file mode 100644 index 000000000..5947839fb Binary files /dev/null and b/public/static/multisrc/madara/bellereservoir/icon.png differ diff --git a/public/static/multisrc/madara/boxnovel/icon.png b/public/static/multisrc/madara/boxnovel/icon.png new file mode 100644 index 000000000..539ebb8df Binary files /dev/null and b/public/static/multisrc/madara/boxnovel/icon.png differ diff --git a/public/static/multisrc/madara/citrusaurora/icon.png b/public/static/multisrc/madara/citrusaurora/icon.png new file mode 100644 index 000000000..e39f2df56 Binary files /dev/null and b/public/static/multisrc/madara/citrusaurora/icon.png differ diff --git a/public/static/multisrc/madara/coralboutique/icon.png b/public/static/multisrc/madara/coralboutique/icon.png new file mode 100644 index 000000000..7bf976b93 Binary files /dev/null and b/public/static/multisrc/madara/coralboutique/icon.png differ diff --git a/public/static/multisrc/madara/daonovel/icon.png b/public/static/multisrc/madara/daonovel/icon.png new file mode 100644 index 000000000..2fa1066de Binary files /dev/null and b/public/static/multisrc/madara/daonovel/icon.png differ diff --git a/public/static/multisrc/madara/dragonholic/icon.png b/public/static/multisrc/madara/dragonholic/icon.png new file mode 100644 index 000000000..7d75e92bc Binary files /dev/null and b/public/static/multisrc/madara/dragonholic/icon.png differ diff --git a/public/static/multisrc/madara/dragontea/icon.png b/public/static/multisrc/madara/dragontea/icon.png new file mode 100644 index 000000000..b1af6de7f Binary files /dev/null and b/public/static/multisrc/madara/dragontea/icon.png differ diff --git a/public/static/multisrc/madara/duskblossoms/icon.png b/public/static/multisrc/madara/duskblossoms/icon.png new file mode 100644 index 000000000..bd1e0a67b Binary files /dev/null and b/public/static/multisrc/madara/duskblossoms/icon.png differ diff --git a/public/static/multisrc/madara/ekitaplar/icon.png b/public/static/multisrc/madara/ekitaplar/icon.png new file mode 100644 index 000000000..ab6cdc3e3 Binary files /dev/null and b/public/static/multisrc/madara/ekitaplar/icon.png differ diff --git a/public/static/multisrc/madara/eternalune/icon.png b/public/static/multisrc/madara/eternalune/icon.png new file mode 100644 index 000000000..456784505 Binary files /dev/null and b/public/static/multisrc/madara/eternalune/icon.png differ diff --git a/public/static/multisrc/madara/etudetranslations/icon.png b/public/static/multisrc/madara/etudetranslations/icon.png new file mode 100644 index 000000000..3016a8f9f Binary files /dev/null and b/public/static/multisrc/madara/etudetranslations/icon.png differ diff --git a/public/static/multisrc/madara/fanstranslations/icon.png b/public/static/multisrc/madara/fanstranslations/icon.png new file mode 100644 index 000000000..2745a85d1 Binary files /dev/null and b/public/static/multisrc/madara/fanstranslations/icon.png differ diff --git a/public/static/multisrc/madara/fortuneeternal/icon.png b/public/static/multisrc/madara/fortuneeternal/icon.png new file mode 100644 index 000000000..52fe996a8 Binary files /dev/null and b/public/static/multisrc/madara/fortuneeternal/icon.png differ diff --git a/public/static/multisrc/madara/foxaholic/icon.png b/public/static/multisrc/madara/foxaholic/icon.png new file mode 100644 index 000000000..036aae427 Binary files /dev/null and b/public/static/multisrc/madara/foxaholic/icon.png differ diff --git a/public/static/multisrc/madara/galaxytranslations/icon.png b/public/static/multisrc/madara/galaxytranslations/icon.png new file mode 100644 index 000000000..efcd822b5 Binary files /dev/null and b/public/static/multisrc/madara/galaxytranslations/icon.png differ diff --git a/public/static/multisrc/madara/guavaread/icon.png b/public/static/multisrc/madara/guavaread/icon.png new file mode 100644 index 000000000..70ff8f488 Binary files /dev/null and b/public/static/multisrc/madara/guavaread/icon.png differ diff --git a/public/static/multisrc/madara/hiraethtranslation/icon.png b/public/static/multisrc/madara/hiraethtranslation/icon.png new file mode 100644 index 000000000..1b497d765 Binary files /dev/null and b/public/static/multisrc/madara/hiraethtranslation/icon.png differ diff --git a/public/static/multisrc/madara/hizomanga/icon.png b/public/static/multisrc/madara/hizomanga/icon.png new file mode 100644 index 000000000..5f58ccad1 Binary files /dev/null and b/public/static/multisrc/madara/hizomanga/icon.png differ diff --git a/public/static/multisrc/madara/kiniga/icon.png b/public/static/multisrc/madara/kiniga/icon.png new file mode 100644 index 000000000..2bc2e882e Binary files /dev/null and b/public/static/multisrc/madara/kiniga/icon.png differ diff --git a/public/static/multisrc/madara/lightnovelupdates/icon.png b/public/static/multisrc/madara/lightnovelupdates/icon.png new file mode 100644 index 000000000..e944d0bed Binary files /dev/null and b/public/static/multisrc/madara/lightnovelupdates/icon.png differ diff --git a/public/static/multisrc/madara/lnheaven/icon.png b/public/static/multisrc/madara/lnheaven/icon.png new file mode 100644 index 000000000..479aacf2d Binary files /dev/null and b/public/static/multisrc/madara/lnheaven/icon.png differ diff --git a/public/static/multisrc/madara/lullobox/icon.png b/public/static/multisrc/madara/lullobox/icon.png new file mode 100644 index 000000000..924c2f572 Binary files /dev/null and b/public/static/multisrc/madara/lullobox/icon.png differ diff --git a/public/static/multisrc/madara/lunarletters/icon.png b/public/static/multisrc/madara/lunarletters/icon.png new file mode 100644 index 000000000..b12938dff Binary files /dev/null and b/public/static/multisrc/madara/lunarletters/icon.png differ diff --git a/public/static/multisrc/madara/markazriwayat/icon.png b/public/static/multisrc/madara/markazriwayat/icon.png new file mode 100644 index 000000000..a2f829263 Binary files /dev/null and b/public/static/multisrc/madara/markazriwayat/icon.png differ diff --git a/public/static/multisrc/madara/massnovel/icon.png b/public/static/multisrc/madara/massnovel/icon.png new file mode 100644 index 000000000..45c667c78 Binary files /dev/null and b/public/static/multisrc/madara/massnovel/icon.png differ diff --git a/public/static/multisrc/madara/meionovel/icon.png b/public/static/multisrc/madara/meionovel/icon.png new file mode 100644 index 000000000..0ceea949f Binary files /dev/null and b/public/static/multisrc/madara/meionovel/icon.png differ diff --git a/public/static/multisrc/madara/meownovel/icon.png b/public/static/multisrc/madara/meownovel/icon.png new file mode 100644 index 000000000..b9f7e19b1 Binary files /dev/null and b/public/static/multisrc/madara/meownovel/icon.png differ diff --git a/public/static/multisrc/madara/morenovel/icon.png b/public/static/multisrc/madara/morenovel/icon.png new file mode 100644 index 000000000..609bd0cd7 Binary files /dev/null and b/public/static/multisrc/madara/morenovel/icon.png differ diff --git a/public/static/multisrc/madara/mostnovel/icon.png b/public/static/multisrc/madara/mostnovel/icon.png new file mode 100644 index 000000000..0005d58e6 Binary files /dev/null and b/public/static/multisrc/madara/mostnovel/icon.png differ diff --git a/public/static/multisrc/madara/mtl-novel/icon.png b/public/static/multisrc/madara/mtl-novel/icon.png new file mode 100644 index 000000000..0791b2697 Binary files /dev/null and b/public/static/multisrc/madara/mtl-novel/icon.png differ diff --git a/public/static/multisrc/madara/mysticalmerries/icon.png b/public/static/multisrc/madara/mysticalmerries/icon.png new file mode 100644 index 000000000..2ea8fe2ba Binary files /dev/null and b/public/static/multisrc/madara/mysticalmerries/icon.png differ diff --git a/public/static/multisrc/madara/nabiscans/icon.png b/public/static/multisrc/madara/nabiscans/icon.png new file mode 100644 index 000000000..cb96cbbf9 Binary files /dev/null and b/public/static/multisrc/madara/nabiscans/icon.png differ diff --git a/public/static/multisrc/madara/neosekaitls/icon.png b/public/static/multisrc/madara/neosekaitls/icon.png new file mode 100644 index 000000000..5de9d7b7d Binary files /dev/null and b/public/static/multisrc/madara/neosekaitls/icon.png differ diff --git a/public/static/multisrc/madara/nitromanga/icon.png b/public/static/multisrc/madara/nitromanga/icon.png new file mode 100644 index 000000000..8b47655b0 Binary files /dev/null and b/public/static/multisrc/madara/nitromanga/icon.png differ diff --git a/public/static/multisrc/madara/noicetranslations/icon.png b/public/static/multisrc/madara/noicetranslations/icon.png new file mode 100644 index 000000000..62a0c104d Binary files /dev/null and b/public/static/multisrc/madara/noicetranslations/icon.png differ diff --git a/public/static/multisrc/madara/novel-lucky/icon.png b/public/static/multisrc/madara/novel-lucky/icon.png new file mode 100644 index 000000000..dd4a629df Binary files /dev/null and b/public/static/multisrc/madara/novel-lucky/icon.png differ diff --git a/public/static/multisrc/madara/novel4up/icon.png b/public/static/multisrc/madara/novel4up/icon.png new file mode 100644 index 000000000..e6692791f Binary files /dev/null and b/public/static/multisrc/madara/novel4up/icon.png differ diff --git a/public/static/multisrc/madara/novelbookid/icon.png b/public/static/multisrc/madara/novelbookid/icon.png new file mode 100644 index 000000000..e34b7952f Binary files /dev/null and b/public/static/multisrc/madara/novelbookid/icon.png differ diff --git a/public/static/multisrc/madara/novelmultiverse/icon.png b/public/static/multisrc/madara/novelmultiverse/icon.png new file mode 100644 index 000000000..1bbe1af97 Binary files /dev/null and b/public/static/multisrc/madara/novelmultiverse/icon.png differ diff --git a/public/static/multisrc/madara/novelninja/icon.png b/public/static/multisrc/madara/novelninja/icon.png new file mode 100644 index 000000000..cbb238614 Binary files /dev/null and b/public/static/multisrc/madara/novelninja/icon.png differ diff --git a/public/static/multisrc/madara/noveloku/icon.png b/public/static/multisrc/madara/noveloku/icon.png new file mode 100644 index 000000000..7ca383e37 Binary files /dev/null and b/public/static/multisrc/madara/noveloku/icon.png differ diff --git a/public/static/multisrc/madara/novelpdf/icon.png b/public/static/multisrc/madara/novelpdf/icon.png new file mode 100644 index 000000000..bbff7c46a Binary files /dev/null and b/public/static/multisrc/madara/novelpdf/icon.png differ diff --git a/public/static/multisrc/madara/noveltl/icon.png b/public/static/multisrc/madara/noveltl/icon.png new file mode 100644 index 000000000..eb2259fde Binary files /dev/null and b/public/static/multisrc/madara/noveltl/icon.png differ diff --git a/public/static/multisrc/madara/olaoe/icon.png b/public/static/multisrc/madara/olaoe/icon.png new file mode 100644 index 000000000..04c75b3ff Binary files /dev/null and b/public/static/multisrc/madara/olaoe/icon.png differ diff --git a/public/static/multisrc/madara/panchotranslations/icon.png b/public/static/multisrc/madara/panchotranslations/icon.png new file mode 100644 index 000000000..f61b6ef21 Binary files /dev/null and b/public/static/multisrc/madara/panchotranslations/icon.png differ diff --git a/public/static/multisrc/madara/pasteltales/icon.png b/public/static/multisrc/madara/pasteltales/icon.png new file mode 100644 index 000000000..7c54b0d77 Binary files /dev/null and b/public/static/multisrc/madara/pasteltales/icon.png differ diff --git a/public/static/multisrc/madara/ragnarscans/icon.png b/public/static/multisrc/madara/ragnarscans/icon.png new file mode 100644 index 000000000..a0187487c Binary files /dev/null and b/public/static/multisrc/madara/ragnarscans/icon.png differ diff --git a/public/static/multisrc/madara/ranovel/icon.png b/public/static/multisrc/madara/ranovel/icon.png new file mode 100644 index 000000000..b2a32c65e Binary files /dev/null and b/public/static/multisrc/madara/ranovel/icon.png differ diff --git a/public/static/multisrc/madara/readfanfic/icon.png b/public/static/multisrc/madara/readfanfic/icon.png new file mode 100644 index 000000000..b5bdbd80c Binary files /dev/null and b/public/static/multisrc/madara/readfanfic/icon.png differ diff --git a/public/static/multisrc/madara/riwyat/icon.png b/public/static/multisrc/madara/riwyat/icon.png new file mode 100644 index 000000000..2f8a5ee28 Binary files /dev/null and b/public/static/multisrc/madara/riwyat/icon.png differ diff --git a/public/static/multisrc/madara/salmonlatte/icon.png b/public/static/multisrc/madara/salmonlatte/icon.png new file mode 100644 index 000000000..18540638f Binary files /dev/null and b/public/static/multisrc/madara/salmonlatte/icon.png differ diff --git a/public/static/multisrc/madara/sleepttls/icon.png b/public/static/multisrc/madara/sleepttls/icon.png new file mode 100644 index 000000000..c9565a9e1 Binary files /dev/null and b/public/static/multisrc/madara/sleepttls/icon.png differ diff --git a/public/static/multisrc/madara/sonicmtl/icon.png b/public/static/multisrc/madara/sonicmtl/icon.png new file mode 100644 index 000000000..a83d7c322 Binary files /dev/null and b/public/static/multisrc/madara/sonicmtl/icon.png differ diff --git a/public/static/multisrc/madara/sweetescape/icon.png b/public/static/multisrc/madara/sweetescape/icon.png new file mode 100644 index 000000000..cb4cfc4de Binary files /dev/null and b/public/static/multisrc/madara/sweetescape/icon.png differ diff --git a/public/static/multisrc/madara/traducciones/icon.png b/public/static/multisrc/madara/traducciones/icon.png new file mode 100644 index 000000000..eb2259fde Binary files /dev/null and b/public/static/multisrc/madara/traducciones/icon.png differ diff --git a/public/static/multisrc/madara/translatinotaku/icon.png b/public/static/multisrc/madara/translatinotaku/icon.png new file mode 100644 index 000000000..d2de4ce61 Binary files /dev/null and b/public/static/multisrc/madara/translatinotaku/icon.png differ diff --git a/public/static/multisrc/madara/turkcelightnovels/icon.png b/public/static/multisrc/madara/turkcelightnovels/icon.png new file mode 100644 index 000000000..f9ba43a6d Binary files /dev/null and b/public/static/multisrc/madara/turkcelightnovels/icon.png differ diff --git a/public/static/multisrc/madara/violetlily/icon.png b/public/static/multisrc/madara/violetlily/icon.png new file mode 100644 index 000000000..3a8646c6c Binary files /dev/null and b/public/static/multisrc/madara/violetlily/icon.png differ diff --git a/public/static/multisrc/madara/wbnovel/icon.png b/public/static/multisrc/madara/wbnovel/icon.png new file mode 100644 index 000000000..d6958644d Binary files /dev/null and b/public/static/multisrc/madara/wbnovel/icon.png differ diff --git a/public/static/multisrc/madara/webnoveloku/icon.png b/public/static/multisrc/madara/webnoveloku/icon.png new file mode 100644 index 000000000..32f65927d Binary files /dev/null and b/public/static/multisrc/madara/webnoveloku/icon.png differ diff --git a/public/static/multisrc/madara/webnovelover/icon.png b/public/static/multisrc/madara/webnovelover/icon.png new file mode 100644 index 000000000..e564462a4 Binary files /dev/null and b/public/static/multisrc/madara/webnovelover/icon.png differ diff --git a/public/static/multisrc/madara/webnoveltraslation/icon.png b/public/static/multisrc/madara/webnoveltraslation/icon.png new file mode 100644 index 000000000..42f910cba Binary files /dev/null and b/public/static/multisrc/madara/webnoveltraslation/icon.png differ diff --git a/public/static/multisrc/madara/wooksteahouse/icon.png b/public/static/multisrc/madara/wooksteahouse/icon.png new file mode 100644 index 000000000..17d0b528d Binary files /dev/null and b/public/static/multisrc/madara/wooksteahouse/icon.png differ diff --git a/public/static/multisrc/madara/wordexcerpt/icon.png b/public/static/multisrc/madara/wordexcerpt/icon.png new file mode 100644 index 000000000..16f4bd655 Binary files /dev/null and b/public/static/multisrc/madara/wordexcerpt/icon.png differ diff --git a/public/static/multisrc/madara/worldnovel/icon.png b/public/static/multisrc/madara/worldnovel/icon.png new file mode 100644 index 000000000..4778f6e30 Binary files /dev/null and b/public/static/multisrc/madara/worldnovel/icon.png differ diff --git a/public/static/multisrc/madara/wuxiaworld.site/icon.png b/public/static/multisrc/madara/wuxiaworld.site/icon.png new file mode 100644 index 000000000..7f62619d5 Binary files /dev/null and b/public/static/multisrc/madara/wuxiaworld.site/icon.png differ diff --git a/public/static/multisrc/madara/zetrotl/icon.png b/public/static/multisrc/madara/zetrotl/icon.png new file mode 100644 index 000000000..4a6ef35da Binary files /dev/null and b/public/static/multisrc/madara/zetrotl/icon.png differ diff --git a/public/static/multisrc/mtlnovel/mtlnovel/icon.png b/public/static/multisrc/mtlnovel/mtlnovel/icon.png new file mode 100644 index 000000000..0ff6056bd Binary files /dev/null and b/public/static/multisrc/mtlnovel/mtlnovel/icon.png differ diff --git a/public/static/multisrc/novelcool/novelcool/icon.png b/public/static/multisrc/novelcool/novelcool/icon.png new file mode 100644 index 000000000..261f4275d Binary files /dev/null and b/public/static/multisrc/novelcool/novelcool/icon.png differ diff --git a/public/static/multisrc/ranobes/ranobes/icon.png b/public/static/multisrc/ranobes/ranobes/icon.png new file mode 100644 index 000000000..38bb73048 Binary files /dev/null and b/public/static/multisrc/ranobes/ranobes/icon.png differ diff --git a/public/static/multisrc/readnovelfull/allnovel/icon.png b/public/static/multisrc/readnovelfull/allnovel/icon.png new file mode 100644 index 000000000..0b5fcb8af Binary files /dev/null and b/public/static/multisrc/readnovelfull/allnovel/icon.png differ diff --git a/public/static/multisrc/readnovelfull/anf.net/icon.png b/public/static/multisrc/readnovelfull/anf.net/icon.png new file mode 100644 index 000000000..9d09ce864 Binary files /dev/null and b/public/static/multisrc/readnovelfull/anf.net/icon.png differ diff --git a/public/static/multisrc/readnovelfull/fwn.com/icon.png b/public/static/multisrc/readnovelfull/fwn.com/icon.png new file mode 100644 index 000000000..924a42990 Binary files /dev/null and b/public/static/multisrc/readnovelfull/fwn.com/icon.png differ diff --git a/public/static/multisrc/readnovelfull/libread/icon.png b/public/static/multisrc/readnovelfull/libread/icon.png new file mode 100644 index 000000000..1cda7270e Binary files /dev/null and b/public/static/multisrc/readnovelfull/libread/icon.png differ diff --git a/public/static/multisrc/readnovelfull/lightnovelplus/icon.png b/public/static/multisrc/readnovelfull/lightnovelplus/icon.png new file mode 100644 index 000000000..a6907dfbc Binary files /dev/null and b/public/static/multisrc/readnovelfull/lightnovelplus/icon.png differ diff --git a/public/static/multisrc/readnovelfull/novelbin/icon.png b/public/static/multisrc/readnovelfull/novelbin/icon.png new file mode 100644 index 000000000..539ebb8df Binary files /dev/null and b/public/static/multisrc/readnovelfull/novelbin/icon.png differ diff --git a/public/static/multisrc/readnovelfull/novelfull/icon.png b/public/static/multisrc/readnovelfull/novelfull/icon.png new file mode 100644 index 000000000..2fc72bd90 Binary files /dev/null and b/public/static/multisrc/readnovelfull/novelfull/icon.png differ diff --git a/public/static/multisrc/readnovelfull/readnovelfull/icon.png b/public/static/multisrc/readnovelfull/readnovelfull/icon.png new file mode 100644 index 000000000..33888dcf9 Binary files /dev/null and b/public/static/multisrc/readnovelfull/readnovelfull/icon.png differ diff --git a/public/static/multisrc/readwn/fannovel/icon.png b/public/static/multisrc/readwn/fannovel/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/fannovel/icon.png differ diff --git a/public/static/multisrc/readwn/ltnovel/icon.png b/public/static/multisrc/readwn/ltnovel/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/ltnovel/icon.png differ diff --git a/public/static/multisrc/readwn/wuxiacity/icon.png b/public/static/multisrc/readwn/wuxiacity/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/wuxiacity/icon.png differ diff --git a/public/static/multisrc/readwn/wuxiamtl/icon.png b/public/static/multisrc/readwn/wuxiamtl/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/wuxiamtl/icon.png differ diff --git a/public/static/multisrc/readwn/wuxiap/icon.png b/public/static/multisrc/readwn/wuxiap/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/wuxiap/icon.png differ diff --git a/public/static/multisrc/readwn/wuxiaspace/icon.png b/public/static/multisrc/readwn/wuxiaspace/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/wuxiaspace/icon.png differ diff --git a/public/static/multisrc/readwn/wuxiav/icon.png b/public/static/multisrc/readwn/wuxiav/icon.png new file mode 100644 index 000000000..60ca0c3ad Binary files /dev/null and b/public/static/multisrc/readwn/wuxiav/icon.png differ diff --git a/public/static/multisrc/rulate/bllate-api/icon.png b/public/static/multisrc/rulate/bllate-api/icon.png new file mode 100644 index 000000000..b92129496 Binary files /dev/null and b/public/static/multisrc/rulate/bllate-api/icon.png differ diff --git a/public/static/multisrc/rulate/erolate-api/icon.png b/public/static/multisrc/rulate/erolate-api/icon.png new file mode 100644 index 000000000..22fa471b4 Binary files /dev/null and b/public/static/multisrc/rulate/erolate-api/icon.png differ diff --git a/public/static/multisrc/rulate/rulate-api/icon.png b/public/static/multisrc/rulate/rulate-api/icon.png new file mode 100644 index 000000000..22fa471b4 Binary files /dev/null and b/public/static/multisrc/rulate/rulate-api/icon.png differ diff --git a/public/static/siteNotAvailable.png b/public/static/siteNotAvailable.png new file mode 100644 index 000000000..1e660b740 Binary files /dev/null and b/public/static/siteNotAvailable.png differ diff --git a/public/static/src/ar/dilartube/icon.png b/public/static/src/ar/dilartube/icon.png new file mode 100644 index 000000000..14e17b394 Binary files /dev/null and b/public/static/src/ar/dilartube/icon.png differ diff --git a/public/static/src/ar/rewayatclub/icon.png b/public/static/src/ar/rewayatclub/icon.png new file mode 100644 index 000000000..5d553d290 Binary files /dev/null and b/public/static/src/ar/rewayatclub/icon.png differ diff --git a/public/static/src/ar/sunovels/icon.png b/public/static/src/ar/sunovels/icon.png new file mode 100644 index 000000000..84264b560 Binary files /dev/null and b/public/static/src/ar/sunovels/icon.png differ diff --git a/public/static/src/cn/69shu/icon.png b/public/static/src/cn/69shu/icon.png new file mode 100644 index 000000000..a4d99fb03 Binary files /dev/null and b/public/static/src/cn/69shu/icon.png differ diff --git a/public/static/src/cn/ixdzs8/favicon.png b/public/static/src/cn/ixdzs8/favicon.png new file mode 100644 index 000000000..702a00e33 Binary files /dev/null and b/public/static/src/cn/ixdzs8/favicon.png differ diff --git a/public/static/src/cn/linovel/icon.png b/public/static/src/cn/linovel/icon.png new file mode 100644 index 000000000..3f830800d Binary files /dev/null and b/public/static/src/cn/linovel/icon.png differ diff --git a/public/static/src/cn/linovelib/icon.png b/public/static/src/cn/linovelib/icon.png new file mode 100644 index 000000000..458be8a38 Binary files /dev/null and b/public/static/src/cn/linovelib/icon.png differ diff --git a/public/static/src/cn/novel543/icon.png b/public/static/src/cn/novel543/icon.png new file mode 100644 index 000000000..c47402f55 Binary files /dev/null and b/public/static/src/cn/novel543/icon.png differ diff --git a/public/static/src/cn/quanben/icon.png b/public/static/src/cn/quanben/icon.png new file mode 100644 index 000000000..ed82912e9 Binary files /dev/null and b/public/static/src/cn/quanben/icon.png differ diff --git a/public/static/src/en/ao3/icon.png b/public/static/src/en/ao3/icon.png new file mode 100644 index 000000000..35a2b245c Binary files /dev/null and b/public/static/src/en/ao3/icon.png differ diff --git a/public/static/src/en/bestlightnovel/icon.png b/public/static/src/en/bestlightnovel/icon.png new file mode 100644 index 000000000..6f3f5d46d Binary files /dev/null and b/public/static/src/en/bestlightnovel/icon.png differ diff --git a/public/static/src/en/chrysanthemumgarden/icon.png b/public/static/src/en/chrysanthemumgarden/icon.png new file mode 100644 index 000000000..be4634dd0 Binary files /dev/null and b/public/static/src/en/chrysanthemumgarden/icon.png differ diff --git a/public/static/src/en/crimsonscrolls/icon.png b/public/static/src/en/crimsonscrolls/icon.png new file mode 100644 index 000000000..bfadff509 Binary files /dev/null and b/public/static/src/en/crimsonscrolls/icon.png differ diff --git a/public/static/src/en/divinedaolibrary/icon.png b/public/static/src/en/divinedaolibrary/icon.png new file mode 100644 index 000000000..3ed57c122 Binary files /dev/null and b/public/static/src/en/divinedaolibrary/icon.png differ diff --git a/public/static/src/en/dreambigtl/icon.png b/public/static/src/en/dreambigtl/icon.png new file mode 100644 index 000000000..071ffa017 Binary files /dev/null and b/public/static/src/en/dreambigtl/icon.png differ diff --git a/public/static/src/en/earlynovel/icon.png b/public/static/src/en/earlynovel/icon.png new file mode 100644 index 000000000..02417a6a3 Binary files /dev/null and b/public/static/src/en/earlynovel/icon.png differ diff --git a/public/static/src/en/faqwikius/icon.png b/public/static/src/en/faqwikius/icon.png new file mode 100644 index 000000000..30fbe21d3 Binary files /dev/null and b/public/static/src/en/faqwikius/icon.png differ diff --git a/public/static/src/en/fenrirrealm/icon.png b/public/static/src/en/fenrirrealm/icon.png new file mode 100644 index 000000000..bdfaf0fb4 Binary files /dev/null and b/public/static/src/en/fenrirrealm/icon.png differ diff --git a/public/static/src/en/fictionzone/icon.png b/public/static/src/en/fictionzone/icon.png new file mode 100644 index 000000000..431a45899 Binary files /dev/null and b/public/static/src/en/fictionzone/icon.png differ diff --git a/public/static/src/en/foxteller/icon.png b/public/static/src/en/foxteller/icon.png new file mode 100644 index 000000000..3cc662654 Binary files /dev/null and b/public/static/src/en/foxteller/icon.png differ diff --git a/public/static/src/en/genesis/customCSS.css b/public/static/src/en/genesis/customCSS.css new file mode 100644 index 000000000..c03768ea3 --- /dev/null +++ b/public/static/src/en/genesis/customCSS.css @@ -0,0 +1,329 @@ +.red-box .red-text, +.blue-box .blue-text, +.gold-box .gold-text, +.silver-box .silver-text { + display:inline-flex; + align-items:start; + justify-content:start; + text-align:left; + flex-direction:column; + margin-left:0; + margin-bottom:0; + margin-top:0 +} +.red-box .red-text p, +.blue-box .blue-text p, +.gold-box .gold-text p, +.silver-box .silver-text p { + text-align:left +} +.ta-center { + display:flex; + text-align:center!important; + align-items:center!important; + justify-content:center!important; + margin-left:auto; + margin-right:auto +} +.ta-between { + align-items:center!important +} +.blue-box, +.red-box, +.gold-box, +.silver-box { + overflow-wrap:break-word; + word-wrap:break-word; + word-break:break-word +} +.blue-box { + display:flex; + flex-direction:column; + justify-content:center; + box-sizing:content-box; + width:auto; + margin:15px auto; + box-shadow:0 0 10px 1px #00219b; + border-radius:5px; + background-color:#000; + padding:20px; + color:#fff; + font-family: Oswald Variable, serif +} +.blue-box div { + text-indent:0; +} +.blue-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.blue-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #004fff,-1px 0 10px #002775; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.blue-text { + display:flex; + flex-direction:row; + align-items:start; + justify-content:center; + text-shadow:-1px 5px 13px #004fff,-1px 0 10px #002775; + font-weight:400; + font-family: Quattrocento, serif +} +.blue-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.red-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #750b00e3; + box-shadow:0 0 20px 3px #750b0070; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif +} +.red-box div { + text-indent:0; +} +.red-box p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.red-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#ff4348; + font-weight:700; + text-shadow:-1px 5px 13px #ff230c,-1px 0px 10px #750b00; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.red-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #ff230c,-1px 0px 10px #750b00; + font-weight:400; + margin-left:auto; + margin-right:auto; + font-family: Quattrocento, serif +} +.red-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.gold-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #e0cc00; + box-shadow:0 0 10px 1px #e0cc00; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif; +} +.gold-box div { + text-indent:0; +} +.gold-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.gold-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #e0cc00,-1px 0px 10px #544d00; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.gold-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-align:center; + text-shadow:-1px 5px 13px #e0cc00,-1px 0 10px #e0cc00; + row-gap:1.5rem; + font-weight:400; + font-family: Quattrocento, serif +} +.gold-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.gold-letter { + display:flex; + flex-direction:column; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #e0cc00,-1px 0 10px #e0cc00; + row-gap:1.5rem; + font-weight:700; + font-family: Amita, serif; + margin-left:auto; + margin-right:auto +} +.gold-letter p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.silver-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #c2c2c2; + box-shadow:0 0 10px 1px #737373; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif; +} +.silver-box div { + text-indent:0; +} +.silver-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.silver-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.silver-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + row-gap:1.5rem; + font-weight:400; + margin: 2rem auto; + font-family: Quattrocento, serif +} +.silver-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.silver-letter { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + row-gap:1.5rem; + font-weight:700; + margin: 2rem auto; + font-family: MedievalSharp, serif +} +.silver-letter p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.blue-text, +.gold-text, +.silver-text, +.red-text, +.blue-box, +.red-box, +.gold-box, +.silver-box, +.red-title, +.gold-title, +.silver-title, +.blue-title, +.blue-text p, +.gold-text p, +.silver-text p, +.red-text p { + text-indent:0!important +} +div.red-text, +div.gold-text, +div.silver-text, +div.blue-text { + display:flex; + flex-direction:column; + margin-bottom:2.25rem; + margin-top:2.25rem +} +.blue-box .blue-text p, +.red-box .red-text p, +.gold-box .gold-text p, +.silver-box .silver-text p { + display:flex; + flex-direction:column +} diff --git a/public/static/src/en/genesis/icon.png b/public/static/src/en/genesis/icon.png new file mode 100644 index 000000000..1cf8159b2 Binary files /dev/null and b/public/static/src/en/genesis/icon.png differ diff --git a/public/static/src/en/indratranslations/icon.png b/public/static/src/en/indratranslations/icon.png new file mode 100644 index 000000000..32e240db4 Binary files /dev/null and b/public/static/src/en/indratranslations/icon.png differ diff --git a/public/static/src/en/inkitt/icon.png b/public/static/src/en/inkitt/icon.png new file mode 100644 index 000000000..2ea24def4 Binary files /dev/null and b/public/static/src/en/inkitt/icon.png differ diff --git a/public/static/src/en/inoveltranslation/icon.png b/public/static/src/en/inoveltranslation/icon.png new file mode 100644 index 000000000..de1405b96 Binary files /dev/null and b/public/static/src/en/inoveltranslation/icon.png differ diff --git a/public/static/src/en/leafstudio/icon.png b/public/static/src/en/leafstudio/icon.png new file mode 100644 index 000000000..c33125de2 Binary files /dev/null and b/public/static/src/en/leafstudio/icon.png differ diff --git a/public/static/src/en/lightnoveltranslations/icon.png b/public/static/src/en/lightnoveltranslations/icon.png new file mode 100644 index 000000000..b24194a55 Binary files /dev/null and b/public/static/src/en/lightnoveltranslations/icon.png differ diff --git a/public/static/src/en/lnmtl/icon.png b/public/static/src/en/lnmtl/icon.png new file mode 100644 index 000000000..f4f437706 Binary files /dev/null and b/public/static/src/en/lnmtl/icon.png differ diff --git a/public/static/src/en/mtlreader/icon.png b/public/static/src/en/mtlreader/icon.png new file mode 100644 index 000000000..e88b98e3b Binary files /dev/null and b/public/static/src/en/mtlreader/icon.png differ diff --git a/public/static/src/en/mvlempyr/icon.png b/public/static/src/en/mvlempyr/icon.png new file mode 100644 index 000000000..fbee1942e Binary files /dev/null and b/public/static/src/en/mvlempyr/icon.png differ diff --git a/public/static/src/en/novelbuddy/icon.png b/public/static/src/en/novelbuddy/icon.png new file mode 100644 index 000000000..794d6e15a Binary files /dev/null and b/public/static/src/en/novelbuddy/icon.png differ diff --git a/public/static/src/en/novelfire/icon.png b/public/static/src/en/novelfire/icon.png new file mode 100644 index 000000000..77d3ee8eb Binary files /dev/null and b/public/static/src/en/novelfire/icon.png differ diff --git a/public/static/src/en/novelhall/icon.png b/public/static/src/en/novelhall/icon.png new file mode 100644 index 000000000..503e9518f Binary files /dev/null and b/public/static/src/en/novelhall/icon.png differ diff --git a/public/static/src/en/novelhi/icon.png b/public/static/src/en/novelhi/icon.png new file mode 100644 index 000000000..4eb688b69 Binary files /dev/null and b/public/static/src/en/novelhi/icon.png differ diff --git a/public/static/src/en/novelight/icon.png b/public/static/src/en/novelight/icon.png new file mode 100644 index 000000000..a1a02276a Binary files /dev/null and b/public/static/src/en/novelight/icon.png differ diff --git a/public/static/src/en/novelrest/icon.png b/public/static/src/en/novelrest/icon.png new file mode 100644 index 000000000..11703697d Binary files /dev/null and b/public/static/src/en/novelrest/icon.png differ diff --git a/public/static/src/en/novelsonline/icon.png b/public/static/src/en/novelsonline/icon.png new file mode 100644 index 000000000..19a2ccf6a Binary files /dev/null and b/public/static/src/en/novelsonline/icon.png differ diff --git a/public/static/src/en/novelupdates/customCSS.css b/public/static/src/en/novelupdates/customCSS.css new file mode 100644 index 000000000..0c809db66 --- /dev/null +++ b/public/static/src/en/novelupdates/customCSS.css @@ -0,0 +1,507 @@ +/** + * Novel Updates -> Daoist Quest + * https://www.novelupdates.com/group/daoist-quest/ + * Necessary to display chapterContent correctly + */ +.litrpg-box { + color: #fff; + font-weight: 400; + text-align: center; + margin: 15px auto; +} +.litrpg-frame { + border: solid #00219b; + box-shadow: 0 0 10px 1px #00219b; + border-radius: 5px; + background-color: #000000; +} +.litrpg-body { + display: flex; + flex-direction: column; + gap: 1.125em; + padding: .875rem 0; + margin: 0 .875rem; + overflow: auto; +} +.litrpg-body ul { + text-align: left; +} + +/** + * Novel Updates -> FictionRead + * https://www.novelupdates.com/group/fictionread/ + * Necessary to display chapterContent correctly + */ +.letter-style { + margin: 15px auto; + box-shadow: 0 0 10px 1px #5e636e; + border-radius: 5px; + font-family: 'Oswald', sans-serif; + background-color: #000000; + padding: 10px 30px 10px 30px; +} +.system { + background: linear-gradient(33deg, #334455 0%, #223344 100%); + border: 1px solid #aabbcc; + color: #fff; + font-family: Rajdhani,serif; + font-weight: 500; + margin: 0 auto; + padding: 1rem; + width: fit-content; +} + +/** + * Novel Updates -> Genesis Studio + * https://www.novelupdates.com/group/genesis-studio/ + * Necessary to display chapterContent correctly + */ +.red-box .red-text, +.blue-box .blue-text, +.gold-box .gold-text, +.silver-box .silver-text { + display:inline-flex; + align-items:start; + justify-content:start; + text-align:left; + flex-direction:column; + margin-left:0; + margin-bottom:0; + margin-top:0 +} +.red-box .red-text p, +.blue-box .blue-text p, +.gold-box .gold-text p, +.silver-box .silver-text p { + text-align:left +} +.ta-center { + display:flex; + text-align:center!important; + align-items:center!important; + justify-content:center!important; + margin-left:auto; + margin-right:auto +} +.ta-between { + align-items:center!important +} +.blue-box, +.red-box, +.gold-box, +.silver-box { + overflow-wrap:break-word; + word-wrap:break-word; + word-break:break-word +} +.blue-box { + display:flex; + flex-direction:column; + justify-content:center; + box-sizing:content-box; + width:auto; + margin:15px auto; + box-shadow:0 0 10px 1px #00219b; + border-radius:5px; + background-color:#000; + padding:20px; + color:#fff; + font-family: Oswald Variable, serif +} +.blue-box div { + text-indent:0; +} +.blue-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.blue-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #004fff,-1px 0 10px #002775; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.blue-text { + display:flex; + flex-direction:row; + align-items:start; + justify-content:center; + text-shadow:-1px 5px 13px #004fff,-1px 0 10px #002775; + font-weight:400; + font-family: Quattrocento, serif +} +.blue-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.red-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #750b00e3; + box-shadow:0 0 20px 3px #750b0070; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif +} +.red-box div { + text-indent:0; +} +.red-box p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.red-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#ff4348; + font-weight:700; + text-shadow:-1px 5px 13px #ff230c,-1px 0px 10px #750b00; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.red-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #ff230c,-1px 0px 10px #750b00; + font-weight:400; + margin-left:auto; + margin-right:auto; + font-family: Quattrocento, serif +} +.red-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.gold-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #e0cc00; + box-shadow:0 0 10px 1px #e0cc00; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif; +} +.gold-box div { + text-indent:0; +} +.gold-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.gold-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #e0cc00,-1px 0px 10px #544d00; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.gold-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-align:center; + text-shadow:-1px 5px 13px #e0cc00,-1px 0 10px #e0cc00; + row-gap:1.5rem; + font-weight:400; + font-family: Quattrocento, serif +} +.gold-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.gold-letter { + display:flex; + flex-direction:column; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #e0cc00,-1px 0 10px #e0cc00; + row-gap:1.5rem; + font-weight:700; + font-family: Amita, serif; + margin-left:auto; + margin-right:auto +} +.gold-letter p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.silver-box { + display:flex; + flex-direction:column; + justify-content:center; + padding:20px; + box-sizing:content-box; + width:auto; + margin:15px auto; + border:solid #c2c2c2; + box-shadow:0 0 10px 1px #737373; + border-radius:5px; + background-color:#000; + color:#fff; + font-family: Oswald Variable, serif; +} +.silver-box div { + text-indent:0; +} +.silver-box p { + text-align:center; + text-indent:0; + margin-bottom:.75rem; + margin-top:.75rem; +} +.silver-title { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + padding-left:0; + margin-left:0; + color:#fff; + font-weight:700; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + border-bottom:3px solid #fff; + padding-bottom:15px; + margin-bottom:15px; +} +.silver-text { + display:flex; + flex-direction:row; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + row-gap:1.5rem; + font-weight:400; + margin: 2rem auto; + font-family: Quattrocento, serif +} +.silver-text p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.silver-letter { + display:flex; + flex-direction:column; + text-align:center; + align-items:center; + justify-content:center; + text-shadow:-1px 5px 13px #b4b4b4,-1px 0 10px #737373; + row-gap:1.5rem; + font-weight:700; + margin: 2rem auto; + font-family: MedievalSharp, serif +} +.silver-letter p { + text-align:center; + text-indent:0; + margin-bottom:.25rem; + margin-top:.25rem; +} +.blue-text, +.gold-text, +.silver-text, +.red-text, +.blue-box, +.red-box, +.gold-box, +.silver-box, +.red-title, +.gold-title, +.silver-title, +.blue-title, +.blue-text p, +.gold-text p, +.silver-text p, +.red-text p { + text-indent:0!important +} +div.red-text, +div.gold-text, +div.silver-text, +div.blue-text { + display:flex; + flex-direction:column; + margin-bottom:2.25rem; + margin-top:2.25rem +} +.blue-box .blue-text p, +.red-box .red-text p, +.gold-box .gold-text p, +.silver-box .silver-text p { + display:flex; + flex-direction:column +} + +/** + * Novel Updates -> Novels Hub + * https://www.novelupdates.com/group/novelshub/ + * Necessary to display chapterContent correctly + */ + .novels-hub_box_orange { + text-align: center; + border: 2px solid #ff6b00; + padding: 20px; + box-shadow: 0 0 15px rgba(255, 107, 0, 0.4); + position: relative; + margin-bottom: 20px; + } + .novels-hub_box-title_orange { + color: #ff6b00; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 2px; + margin-bottom: 15px; + text-shadow: 0 0 10px rgba(255, 107, 0, 0.6); + } + .novels-hub_box-text_orange { + color: white; + line-height: 1.6; + border-top: 1px solid #ff6b00; + padding-top: 15px; + text-shadow: 0 0 5px rgba(255, 255, 255, 0.3); + } + .novels-hub_box_green { + background: #0d0d0d; + border: 2px solid #00ff88; + padding: 15px 20px; + box-shadow: 0 0 10px rgba(0, 255, 136, 0.3); + margin-bottom: 15px; + } + .novels-hub_box-title_green { + color: #00ff88; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 1px; + text-shadow: 0 0 8px rgba(0, 255, 136, 0.5); + } + .novels-hub_comment_green { + background: linear-gradient(135deg, #1a1a1a 0%, #0f0f0f 100%); + border-left: 4px solid #00ff88; + padding: 15px 20px; + margin-bottom: 15px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); + } + .novels-hub_box_blue { + background: rgba(0, 33, 155, 0.15); + border: 1px solid #0066ff; + border-radius: 4px; + padding: 15px 20px; + margin-bottom: 15px; + backdrop-filter: blur(5px); + box-shadow: inset 0 0 10px rgba(0, 102, 255, 0.1); + } + .novels-hub_box-title_blue { + color: #0099ff; + margin-bottom: 8px; + text-transform: uppercase; + letter-spacing: 1px; + } + .novels-hub_box-text_blue { + color: #d0d0d0; + line-height: 1.6; + } + .novels-hub_text_red { + color: #ff6b6b; + } + .novels-hub_text_blue { + color: #4d9fff; + } + .novels-hub_text_purple { + color: #a78bfa; + } + +/** + * Novel Updates -> Raei Translations + * https://www.novelupdates.com/group/raei-translations/ + * Necessary to display chapterContent correctly + */ +.status { + --color-special: #ff751a; + --color-special-light: #ffa970; + border-left: ridge 4px var(--color-special); + padding: 1em; + color: var(--color-special-light); + box-shadow: -5px 0 4px -5px var(--color-special); + margin-top: 2em; + margin-bottom: 2em; + white-space: pre-wrap; + word-wrap: break-word; +} + +/** + * Novel Updates -> Sky Demon Order + * https://www.novelupdates.com/group/sky-demon-order/ + * Necessary to display chapterContent correctly + */ +.novel-system-box { + --colors-novel-system-box-bg-1: 0 2 20; + --colors-novel-system-box-bg-2: 34 40 93; + --colors-novel-system-box-border: 137 149 199; + --colors-novel-system-box-shadow: 9 23 134; + background: linear-gradient(180deg,rgb(var(--colors-novel-system-box-bg-1)) 35%,rgb(var(--colors-novel-system-box-bg-2)) 214%); + border: 1px solid rgb(var(--colors-novel-system-box-border)); + color: #fff; + font-family: Rajdhani,serif; + font-weight: 500; + margin: 0 auto; + padding: 1rem; + text-shadow: 1px 1px 2px rgb(var(--colors-novel-system-box-shadow)),0 0 1em rgb(var(--colors-novel-system-box-shadow)),0 0 .2em rgb(var(--colors-novel-system-box-shadow)); + width: fit-content; +} +.text-center { + text-align: center !important; +} diff --git a/public/static/src/en/novelupdates/icon.png b/public/static/src/en/novelupdates/icon.png new file mode 100644 index 000000000..a58a76667 Binary files /dev/null and b/public/static/src/en/novelupdates/icon.png differ diff --git a/public/static/src/en/pawread/icon.png b/public/static/src/en/pawread/icon.png new file mode 100644 index 000000000..906724299 Binary files /dev/null and b/public/static/src/en/pawread/icon.png differ diff --git a/public/static/src/en/rainofsnow/icon.png b/public/static/src/en/rainofsnow/icon.png new file mode 100644 index 000000000..43c8de8b7 Binary files /dev/null and b/public/static/src/en/rainofsnow/icon.png differ diff --git a/public/static/src/en/readfrom/icon.png b/public/static/src/en/readfrom/icon.png new file mode 100644 index 000000000..1f6cb823f Binary files /dev/null and b/public/static/src/en/readfrom/icon.png differ diff --git a/public/static/src/en/readlitenovel/icon.png b/public/static/src/en/readlitenovel/icon.png new file mode 100644 index 000000000..5e6f79701 Binary files /dev/null and b/public/static/src/en/readlitenovel/icon.png differ diff --git a/public/static/src/en/reaperscans/icon.png b/public/static/src/en/reaperscans/icon.png new file mode 100644 index 000000000..440bac9d4 Binary files /dev/null and b/public/static/src/en/reaperscans/icon.png differ diff --git a/public/static/src/en/relibrary/icon.png b/public/static/src/en/relibrary/icon.png new file mode 100644 index 000000000..c29418562 Binary files /dev/null and b/public/static/src/en/relibrary/icon.png differ diff --git a/public/static/src/en/royalroad/icon.png b/public/static/src/en/royalroad/icon.png new file mode 100644 index 000000000..ba9d783b9 Binary files /dev/null and b/public/static/src/en/royalroad/icon.png differ diff --git a/public/static/src/en/scribblehub/icon.png b/public/static/src/en/scribblehub/icon.png new file mode 100644 index 000000000..6f9adfcc6 Binary files /dev/null and b/public/static/src/en/scribblehub/icon.png differ diff --git a/public/static/src/en/storyseedling/icon.png b/public/static/src/en/storyseedling/icon.png new file mode 100644 index 000000000..eb2259fde Binary files /dev/null and b/public/static/src/en/storyseedling/icon.png differ diff --git a/public/static/src/en/vynovel/icon.png b/public/static/src/en/vynovel/icon.png new file mode 100644 index 000000000..3467d0713 Binary files /dev/null and b/public/static/src/en/vynovel/icon.png differ diff --git a/public/static/src/en/wct/icon.png b/public/static/src/en/wct/icon.png new file mode 100644 index 000000000..9e3d9ceb7 Binary files /dev/null and b/public/static/src/en/wct/icon.png differ diff --git a/public/static/src/en/webnovel/icon.png b/public/static/src/en/webnovel/icon.png new file mode 100644 index 000000000..da548d90f Binary files /dev/null and b/public/static/src/en/webnovel/icon.png differ diff --git a/public/static/src/en/wtrlab/icon.png b/public/static/src/en/wtrlab/icon.png new file mode 100644 index 000000000..3b323cad0 Binary files /dev/null and b/public/static/src/en/wtrlab/icon.png differ diff --git a/public/static/src/en/wuxiaworld/icon.png b/public/static/src/en/wuxiaworld/icon.png new file mode 100644 index 000000000..e58177b8d Binary files /dev/null and b/public/static/src/en/wuxiaworld/icon.png differ diff --git a/public/static/src/es/hasutl/icon.jpg b/public/static/src/es/hasutl/icon.jpg new file mode 100644 index 000000000..50c14648b Binary files /dev/null and b/public/static/src/es/hasutl/icon.jpg differ diff --git a/public/static/src/es/nova/icon.png b/public/static/src/es/nova/icon.png new file mode 100644 index 000000000..7c70fbfdd Binary files /dev/null and b/public/static/src/es/nova/icon.png differ diff --git a/public/static/src/es/novelasligera/icon.png b/public/static/src/es/novelasligera/icon.png new file mode 100644 index 000000000..609f28718 Binary files /dev/null and b/public/static/src/es/novelasligera/icon.png differ diff --git a/public/static/src/es/novelyra/icon.png b/public/static/src/es/novelyra/icon.png new file mode 100644 index 000000000..5526fadc2 Binary files /dev/null and b/public/static/src/es/novelyra/icon.png differ diff --git a/public/static/src/es/oasistranslations/icon.png b/public/static/src/es/oasistranslations/icon.png new file mode 100644 index 000000000..27dd12e7a Binary files /dev/null and b/public/static/src/es/oasistranslations/icon.png differ diff --git a/public/static/src/es/reinowuxia/icon.png b/public/static/src/es/reinowuxia/icon.png new file mode 100644 index 000000000..af71fa47c Binary files /dev/null and b/public/static/src/es/reinowuxia/icon.png differ diff --git a/public/static/src/es/skynovels/icon.png b/public/static/src/es/skynovels/icon.png new file mode 100644 index 000000000..5b125d558 Binary files /dev/null and b/public/static/src/es/skynovels/icon.png differ diff --git a/public/static/src/es/tunovelaligera/icon.png b/public/static/src/es/tunovelaligera/icon.png new file mode 100644 index 000000000..594cebb67 Binary files /dev/null and b/public/static/src/es/tunovelaligera/icon.png differ diff --git a/public/static/src/es/yuukitls/icon.png b/public/static/src/es/yuukitls/icon.png new file mode 100644 index 000000000..277b7d729 Binary files /dev/null and b/public/static/src/es/yuukitls/icon.png differ diff --git a/public/static/src/fr/chireads/icon.png b/public/static/src/fr/chireads/icon.png new file mode 100644 index 000000000..217c4e17d Binary files /dev/null and b/public/static/src/fr/chireads/icon.png differ diff --git a/public/static/src/fr/harkeneliwood/icon.png b/public/static/src/fr/harkeneliwood/icon.png new file mode 100644 index 000000000..db4ee109e Binary files /dev/null and b/public/static/src/fr/harkeneliwood/icon.png differ diff --git a/public/static/src/fr/kisswood/icon.png b/public/static/src/fr/kisswood/icon.png new file mode 100644 index 000000000..8f1b9181e Binary files /dev/null and b/public/static/src/fr/kisswood/icon.png differ diff --git a/public/static/src/fr/noveldeglace/icon.png b/public/static/src/fr/noveldeglace/icon.png new file mode 100644 index 000000000..a81b27691 Binary files /dev/null and b/public/static/src/fr/noveldeglace/icon.png differ diff --git a/public/static/src/fr/novhell/icon.png b/public/static/src/fr/novhell/icon.png new file mode 100644 index 000000000..22fade294 Binary files /dev/null and b/public/static/src/fr/novhell/icon.png differ diff --git a/public/static/src/fr/phenixscans/icon.png b/public/static/src/fr/phenixscans/icon.png new file mode 100644 index 000000000..e4a836c0b Binary files /dev/null and b/public/static/src/fr/phenixscans/icon.png differ diff --git a/public/static/src/fr/warriorlegendtrad/icon.png b/public/static/src/fr/warriorlegendtrad/icon.png new file mode 100644 index 000000000..1b15a1cf7 Binary files /dev/null and b/public/static/src/fr/warriorlegendtrad/icon.png differ diff --git a/public/static/src/fr/wuxialnscantrad/icon.png b/public/static/src/fr/wuxialnscantrad/icon.png new file mode 100644 index 000000000..fb432888b Binary files /dev/null and b/public/static/src/fr/wuxialnscantrad/icon.png differ diff --git a/public/static/src/fr/xiaowaz/icon.png b/public/static/src/fr/xiaowaz/icon.png new file mode 100644 index 000000000..32275a07f Binary files /dev/null and b/public/static/src/fr/xiaowaz/icon.png differ diff --git a/public/static/src/id/indowebnovel/icon.png b/public/static/src/id/indowebnovel/icon.png new file mode 100644 index 000000000..2ab4c5b58 Binary files /dev/null and b/public/static/src/id/indowebnovel/icon.png differ diff --git a/public/static/src/id/novelringan/icon.png b/public/static/src/id/novelringan/icon.png new file mode 100644 index 000000000..6724a6087 Binary files /dev/null and b/public/static/src/id/novelringan/icon.png differ diff --git a/public/static/src/id/sakuranovel/icon.png b/public/static/src/id/sakuranovel/icon.png new file mode 100644 index 000000000..8ecb90055 Binary files /dev/null and b/public/static/src/id/sakuranovel/icon.png differ diff --git a/public/static/src/jp/kakuyomu/icon.png b/public/static/src/jp/kakuyomu/icon.png new file mode 100644 index 000000000..7bbf96ec3 Binary files /dev/null and b/public/static/src/jp/kakuyomu/icon.png differ diff --git a/public/static/src/jp/syosetu/icon.png b/public/static/src/jp/syosetu/icon.png new file mode 100644 index 000000000..37a4b6f92 Binary files /dev/null and b/public/static/src/jp/syosetu/icon.png differ diff --git a/public/static/src/kr/agitoon/icon.png b/public/static/src/kr/agitoon/icon.png new file mode 100644 index 000000000..8b6f8f863 Binary files /dev/null and b/public/static/src/kr/agitoon/icon.png differ diff --git a/public/static/src/multi/komga/icon.png b/public/static/src/multi/komga/icon.png new file mode 100644 index 000000000..e18c8f4a4 Binary files /dev/null and b/public/static/src/multi/komga/icon.png differ diff --git a/public/static/src/pl/novelki/icon.png b/public/static/src/pl/novelki/icon.png new file mode 100644 index 000000000..3c6346868 Binary files /dev/null and b/public/static/src/pl/novelki/icon.png differ diff --git a/public/static/src/pt-br/blogdoamonnovels/icon.png b/public/static/src/pt-br/blogdoamonnovels/icon.png new file mode 100644 index 000000000..c32ba07e1 Binary files /dev/null and b/public/static/src/pt-br/blogdoamonnovels/icon.png differ diff --git a/public/static/src/pt-br/illusia/icon.png b/public/static/src/pt-br/illusia/icon.png new file mode 100644 index 000000000..f4a9b08e8 Binary files /dev/null and b/public/static/src/pt-br/illusia/icon.png differ diff --git a/public/static/src/pt-br/novelmania/icon.png b/public/static/src/pt-br/novelmania/icon.png new file mode 100644 index 000000000..38473e889 Binary files /dev/null and b/public/static/src/pt-br/novelmania/icon.png differ diff --git a/public/static/src/pt-br/tsundoku/icon.png b/public/static/src/pt-br/tsundoku/icon.png new file mode 100644 index 000000000..d0077229e Binary files /dev/null and b/public/static/src/pt-br/tsundoku/icon.png differ diff --git a/public/static/src/ru/authortoday/icon.png b/public/static/src/ru/authortoday/icon.png new file mode 100644 index 000000000..6790ba067 Binary files /dev/null and b/public/static/src/ru/authortoday/icon.png differ diff --git a/public/static/src/ru/bookriver/icon.png b/public/static/src/ru/bookriver/icon.png new file mode 100644 index 000000000..efaacf492 Binary files /dev/null and b/public/static/src/ru/bookriver/icon.png differ diff --git a/public/static/src/ru/ficbook/icon.png b/public/static/src/ru/ficbook/icon.png new file mode 100644 index 000000000..5e6db1fe3 Binary files /dev/null and b/public/static/src/ru/ficbook/icon.png differ diff --git a/public/static/src/ru/freedlit/icon.png b/public/static/src/ru/freedlit/icon.png new file mode 100644 index 000000000..2d1f74961 Binary files /dev/null and b/public/static/src/ru/freedlit/icon.png differ diff --git a/public/static/src/ru/jaomix/icon.png b/public/static/src/ru/jaomix/icon.png new file mode 100644 index 000000000..a6e43f575 Binary files /dev/null and b/public/static/src/ru/jaomix/icon.png differ diff --git a/public/static/src/ru/neobook/icon.png b/public/static/src/ru/neobook/icon.png new file mode 100644 index 000000000..22ce46698 Binary files /dev/null and b/public/static/src/ru/neobook/icon.png differ diff --git a/public/static/src/ru/novelovh/icon.png b/public/static/src/ru/novelovh/icon.png new file mode 100644 index 000000000..1636753c3 Binary files /dev/null and b/public/static/src/ru/novelovh/icon.png differ diff --git a/public/static/src/ru/noveltl/icon.png b/public/static/src/ru/noveltl/icon.png new file mode 100644 index 000000000..d188c3144 Binary files /dev/null and b/public/static/src/ru/noveltl/icon.png differ diff --git a/public/static/src/ru/ranobehub/icon.png b/public/static/src/ru/ranobehub/icon.png new file mode 100644 index 000000000..78253b2ce Binary files /dev/null and b/public/static/src/ru/ranobehub/icon.png differ diff --git a/public/static/src/ru/ranobelib/icon.png b/public/static/src/ru/ranobelib/icon.png new file mode 100644 index 000000000..2bce5a1a3 Binary files /dev/null and b/public/static/src/ru/ranobelib/icon.png differ diff --git a/public/static/src/ru/ranoberf/icon.png b/public/static/src/ru/ranoberf/icon.png new file mode 100644 index 000000000..34fd7bd5d Binary files /dev/null and b/public/static/src/ru/ranoberf/icon.png differ diff --git a/public/static/src/ru/renovels/icon.png b/public/static/src/ru/renovels/icon.png new file mode 100644 index 000000000..d0dd0409f Binary files /dev/null and b/public/static/src/ru/renovels/icon.png differ diff --git a/public/static/src/ru/ruvers/icon.png b/public/static/src/ru/ruvers/icon.png new file mode 100644 index 000000000..7f1de8aeb Binary files /dev/null and b/public/static/src/ru/ruvers/icon.png differ diff --git a/public/static/src/ru/topliba/icon.png b/public/static/src/ru/topliba/icon.png new file mode 100644 index 000000000..79f34ced3 Binary files /dev/null and b/public/static/src/ru/topliba/icon.png differ diff --git a/public/static/src/ru/zelluloza/icon.png b/public/static/src/ru/zelluloza/icon.png new file mode 100644 index 000000000..1b7b465a4 Binary files /dev/null and b/public/static/src/ru/zelluloza/icon.png differ diff --git a/public/static/src/tr/epiknovel/icon.png b/public/static/src/tr/epiknovel/icon.png new file mode 100644 index 000000000..c7b779d40 Binary files /dev/null and b/public/static/src/tr/epiknovel/icon.png differ diff --git a/public/static/src/tr/mangatr/icon.png b/public/static/src/tr/mangatr/icon.png new file mode 100644 index 000000000..8c2dba709 Binary files /dev/null and b/public/static/src/tr/mangatr/icon.png differ diff --git a/public/static/src/uk/bakainua/icon.png b/public/static/src/uk/bakainua/icon.png new file mode 100644 index 000000000..d1dc0ec45 Binary files /dev/null and b/public/static/src/uk/bakainua/icon.png differ diff --git a/public/static/src/uk/smakolykytl/icon.png b/public/static/src/uk/smakolykytl/icon.png new file mode 100644 index 000000000..10b7e01a6 Binary files /dev/null and b/public/static/src/uk/smakolykytl/icon.png differ diff --git a/public/static/src/uk/uaranobeclub/icon.png b/public/static/src/uk/uaranobeclub/icon.png new file mode 100644 index 000000000..f7bedada8 Binary files /dev/null and b/public/static/src/uk/uaranobeclub/icon.png differ diff --git a/public/static/src/vi/hakolightnovel/icon.png b/public/static/src/vi/hakolightnovel/icon.png new file mode 100644 index 000000000..203b467e2 Binary files /dev/null and b/public/static/src/vi/hakolightnovel/icon.png differ diff --git a/public/static/src/vi/lightnovelvn/icon.png b/public/static/src/vi/lightnovelvn/icon.png new file mode 100644 index 000000000..ef1ce2815 Binary files /dev/null and b/public/static/src/vi/lightnovelvn/icon.png differ diff --git a/public/static/src/vi/nettruyen/icon.png b/public/static/src/vi/nettruyen/icon.png new file mode 100644 index 000000000..db5d0538f Binary files /dev/null and b/public/static/src/vi/nettruyen/icon.png differ diff --git a/public/static/src/vi/truyenchu/icon.png b/public/static/src/vi/truyenchu/icon.png new file mode 100644 index 000000000..2e37c2dce Binary files /dev/null and b/public/static/src/vi/truyenchu/icon.png differ diff --git a/public/static/src/vi/truyenconect/icon.png b/public/static/src/vi/truyenconect/icon.png new file mode 100644 index 000000000..e70eb5b41 Binary files /dev/null and b/public/static/src/vi/truyenconect/icon.png differ diff --git a/public/static/src/vi/truyenconect/test.js b/public/static/src/vi/truyenconect/test.js new file mode 100644 index 000000000..3338d2e88 --- /dev/null +++ b/public/static/src/vi/truyenconect/test.js @@ -0,0 +1 @@ +// var test = 2; diff --git a/public/static/src/vi/truyenfull/icon.png b/public/static/src/vi/truyenfull/icon.png new file mode 100644 index 000000000..02417a6a3 Binary files /dev/null and b/public/static/src/vi/truyenfull/icon.png differ diff --git a/public/static/src/vi/truyenss/icon.png b/public/static/src/vi/truyenss/icon.png new file mode 100644 index 000000000..db5d0538f Binary files /dev/null and b/public/static/src/vi/truyenss/icon.png differ diff --git a/scripts/build-plugin-manifest.js b/scripts/build-plugin-manifest.js new file mode 100644 index 000000000..cab5eec47 --- /dev/null +++ b/scripts/build-plugin-manifest.js @@ -0,0 +1,239 @@ +import path from 'path'; +import fs from 'fs'; +import languages from './languages.js'; +import { execSync } from 'child_process'; +import { minify } from './terser.js'; + +const REMOTE = execSync('git remote get-url origin') + .toString() + .replace(/[\s\n]/g, ''); +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}/${BRANCH}`; + +const STATIC_LINK = `${USER_CONTENT_LINK}/public/static`; +// Use legacy .js/src/plugins path for backward compatibility +const PLUGIN_LINK = `${USER_CONTENT_LINK}/.js/src/plugins`; + +const DIST_DIR = '.dist'; + +let json = []; +if (!fs.existsSync(DIST_DIR)) { + fs.mkdirSync(DIST_DIR); +} +const jsonPath = path.join(DIST_DIR, 'plugins.json'); +const jsonMinPath = path.join(DIST_DIR, 'plugins.min.json'); +const pluginSet = new Set(); +const pluginsPerLanguage = {}; +const pluginsWithFiltersPerLanguage = {}; + +const args = process.argv.slice(2); +let ONLY_NEW = args.includes('--only-new'); + +let existingPlugins = {}; +if (!fs.existsSync(jsonPath)) ONLY_NEW = false; +if (ONLY_NEW) { + try { + const existingJson = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')); + json = existingJson; + for (const plugin of existingJson) { + existingPlugins[plugin.id] = plugin; + } + } catch (e) { + console.warn('Failed to parse existing plugins.json:', e); + } +} + +// Simple semver comparison: "1.2.3" < "1.2.4" +function compareVersions(a, b) { + const pa = a.split('.').map(Number); + const pb = b.split('.').map(Number); + for (let i = 0; i < Math.max(pa.length, pb.length); i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na > nb) return 1; + if (na < nb) return -1; + } + return 0; +} + +const createRecursiveProxy = () => { + const target = {}; + const handler = { + get(target, prop) { + if (prop === 'get') { + return a => a; + } + if (!target[prop]) { + target[prop] = createRecursiveProxy(); + } + return target[prop]; + }, + }; + return new Proxy(target, handler); +}; + +const proxy = createRecursiveProxy(); + +const _require = () => proxy; + +const COMPILED_PLUGIN_DIR = './.js/plugins'; + +for (let language in languages) { + console.log( + ` ${language} ` + .padStart(Math.floor((language.length + 32) / 2), '=') + .padEnd(30, '='), + ); + + const langPath = path.join(COMPILED_PLUGIN_DIR, language.toLowerCase()); + if (!fs.existsSync(langPath)) continue; + const plugins = fs.readdirSync(langPath); + + pluginsPerLanguage[language] = 0; + pluginsWithFiltersPerLanguage[language] = 0; + + plugins.forEach(plugin => { + if (plugin.startsWith('.')) return; + minify(path.join(langPath, plugin)); + const rawCode = fs.readFileSync( + `${COMPILED_PLUGIN_DIR}/${language.toLowerCase()}/${plugin}`, + 'utf-8', + ); + const instance = Function( + 'require', + 'module', + `const exports = module.exports = {}; + ${rawCode}; + return exports.default`, + )(_require, {}); + const { id, name, site, version, icon, customJS, customCSS, filters } = + instance; + const normalisedName = name.replace(/\[.*\]/, ''); + + // --only-new logic + if ( + ONLY_NEW && + existingPlugins[id] && + compareVersions(existingPlugins[id].version, version) >= 0 + ) { + // console.log(` Skipping ${name} (${id}) - not newer`, '\r🔁'); + return; + } + + const info = { + id, + name: normalisedName, + site, + lang: languages[language], + version, + url: `${PLUGIN_LINK}/${language.toLowerCase()}/${plugin}`, + iconUrl: `${STATIC_LINK}/${icon || 'siteNotAvailable.png'}`, + customJS: customJS ? `${STATIC_LINK}/${customJS}` : undefined, + customCSS: customCSS ? `${STATIC_LINK}/${customCSS}` : undefined, + }; + + if (pluginSet.has(id)) { + console.log("There's already a plugin with id:", id); + throw new Error('2 or more plugins have the same id'); + } else { + pluginSet.add(id); + } + json.push(info); + + pluginsPerLanguage[language] += 1; + if (filters !== undefined) { + pluginsWithFiltersPerLanguage[language] += 1; + } + + console.log( + ' ', + name.padEnd(25), + ` (${id})`, + filters == undefined ? '\r✅' : '\r✅🔍', + ); + }); +} + +json.sort((a, b) => { + if (a.lang === b.lang) return a.id.localeCompare(b.id); + return 0; +}); + +fs.writeFileSync(jsonMinPath, JSON.stringify(json)); +fs.writeFileSync(jsonPath, JSON.stringify(json, null, '\t')); + +const totalPlugins = Object.values(pluginsPerLanguage).reduce( + (a, b) => a + b, + 0, +); +if (!ONLY_NEW) + fs.writeFileSync( + 'total.svg', + ` + <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="80" height="20" role="img" aria-label="Plugins: ${totalPlugins}"> + <title>Plugins: ${totalPlugins} + + + + + + + + + + + + + + + plugins + + ${totalPlugins} + + + `, + ); + +// check for broken plugins +for (let language in languages) { + const tsFiles = fs.readdirSync( + path.join('./plugins', language.toLocaleLowerCase()), + ); + tsFiles + .filter(f => f.endsWith('.broken.ts')) + .forEach(fn => { + console.error( + language.toLocaleLowerCase() + + '/' + + fn.replace('.broken.ts', '') + + ' ❌', + ); + }); +} + +console.log(jsonPath); +console.log('Done ✅'); + +const totalPluginsWithFilter = Object.values( + pluginsWithFiltersPerLanguage, +).reduce((a, b) => a + b, 0); + +// Markdown table for GitHub Actions +console.warn('\n| Language | Plugins (With Filters) |'); +console.warn('|----------|------------------------|'); +for (const language of Object.keys(languages)) { + console.warn( + `| ${language} | ${pluginsPerLanguage[language] || 0} (${pluginsWithFiltersPerLanguage[language] || 0}) |`, + ); +} +console.warn('|----------|------------------------|'); +console.warn(`| Total | ${totalPlugins} (${totalPluginsWithFilter}) |`); diff --git a/scripts/check-plugin-sites.js b/scripts/check-plugin-sites.js new file mode 100644 index 000000000..eeb9f7ee5 --- /dev/null +++ b/scripts/check-plugin-sites.js @@ -0,0 +1,337 @@ +#!/usr/bin/env node + +import https from 'https'; +import http from 'http'; +import { URL, fileURLToPath } from 'url'; +import fs from 'fs'; +import path, { dirname } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const TIMEOUT = 10000; +const CONCURRENT_REQUESTS = 10; +const PLUGINS_JSON_URL = + 'https://raw.githubusercontent.com/LNReader/lnreader-plugins/refs/heads/plugins/v3.0.0/.dist/plugins.json'; + +const results = { + accessible: [], + inaccessible: [], + cloudflare: [], + errors: [], +}; + +let totalSites = 0; +let checkedSites = 0; + +function checkSite(url, pluginInfo) { + return new Promise(resolve => { + const parsedUrl = new URL(url); + const isHttps = parsedUrl.protocol === 'https:'; + const client = isHttps ? https : http; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: 'HEAD', + timeout: TIMEOUT, + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + }; + + const req = client.request(options, res => { + const headers = res.headers || {}; + const isCloudflare = + headers['cf-ray'] || + headers['server']?.toLowerCase().includes('cloudflare') || + headers['cf-cache-status'] || + headers['cf-request-id'] || + (res.statusCode === 403 && + (headers['server']?.toLowerCase().includes('cloudflare') || + headers['cf-ray'])); + + if (res.statusCode >= 200 && res.statusCode < 400) { + resolve({ + status: 'accessible', + statusCode: res.statusCode, + url, + pluginInfo, + }); + } else if (res.statusCode === 403 && isCloudflare) { + resolve({ + status: 'cloudflare', + statusCode: res.statusCode, + url, + pluginInfo, + reason: 'HTTP 403 (Cloudflare protected)', + }); + } else { + resolve({ + status: 'inaccessible', + statusCode: res.statusCode, + url, + pluginInfo, + reason: `HTTP ${res.statusCode}`, + }); + } + res.destroy(); + }); + + req.on('error', error => { + resolve({ + status: 'inaccessible', + url, + pluginInfo, + reason: error.message || 'Connection error', + error: error.code || 'UNKNOWN', + }); + }); + + req.on('timeout', () => { + req.destroy(); + resolve({ + status: 'inaccessible', + url, + pluginInfo, + reason: 'Request timeout', + error: 'TIMEOUT', + }); + }); + + req.setTimeout(TIMEOUT); + req.end(); + }); +} + +async function processSites(sites) { + const queue = [...sites]; + + async function processNext() { + if (queue.length === 0) { + return; + } + + const { url, pluginInfo } = queue.shift(); + + try { + const result = await checkSite(url, pluginInfo); + checkedSites++; + + if (result.status === 'accessible') { + results.accessible.push(result); + process.stdout.write( + `\r✓ ${checkedSites}/${totalSites} - ${pluginInfo.name || url}`, + ); + } else if (result.status === 'cloudflare') { + results.cloudflare.push(result); + process.stdout.write( + `\r⚠ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (Cloudflare)`, + ); + } else { + results.inaccessible.push(result); + process.stdout.write( + `\r✗ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (${result.reason})`, + ); + } + } catch (error) { + checkedSites++; + results.errors.push({ + url, + pluginInfo, + error: error.message, + }); + process.stdout.write( + `\r✗ ${checkedSites}/${totalSites} - ${pluginInfo.name || url} (Error)`, + ); + } finally { + await processNext(); + } + } + + const promises = []; + for (let i = 0; i < CONCURRENT_REQUESTS && i < sites.length; i++) { + promises.push(processNext()); + } + + await Promise.all(promises); +} + +function fetchJson(url) { + return new Promise((resolve, reject) => { + const parsedUrl = new URL(url); + const client = parsedUrl.protocol === 'https:' ? https : http; + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80), + path: parsedUrl.pathname + parsedUrl.search, + method: 'GET', + headers: { + 'User-Agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + }, + }; + + const req = client.request(options, res => { + if (res.statusCode !== 200) { + reject( + new Error(`Failed to fetch plugins.json: HTTP ${res.statusCode}`), + ); + return; + } + + let data = ''; + res.on('data', chunk => { + data += chunk; + }); + + res.on('end', () => { + try { + resolve(JSON.parse(data)); + } catch (error) { + reject(new Error(`Failed to parse JSON: ${error.message}`)); + } + }); + }); + + req.on('error', error => { + reject(new Error(`Request failed: ${error.message}`)); + }); + + req.setTimeout(30000, () => { + req.destroy(); + reject(new Error('Request timeout')); + }); + + req.end(); + }); +} + +async function main() { + console.log('Fetching plugins.json from GitHub...\n'); + + try { + const plugins = await fetchJson(PLUGINS_JSON_URL); + console.log(`Successfully fetched ${plugins.length} plugins.\n`); + + const siteMap = new Map(); + plugins.forEach(plugin => { + const site = plugin.site; + if (site && site !== 'url' && site.trim() !== '') { + if (!siteMap.has(site)) { + siteMap.set(site, { + url: site, + pluginInfo: { + id: plugin.id, + name: plugin.name, + lang: plugin.lang, + }, + }); + } + } + }); + + const sites = Array.from(siteMap.values()); + totalSites = sites.length; + + console.log(`Found ${totalSites} unique sites to check.\n`); + console.log('Checking sites...\n'); + + await processSites(sites); + + console.log('\n\n' + '='.repeat(80)); + console.log('RESULTS SUMMARY'); + console.log('='.repeat(80)); + console.log(`Total sites checked: ${totalSites}`); + console.log(`Accessible: ${results.accessible.length}`); + console.log(`Cloudflare protected: ${results.cloudflare.length}`); + console.log(`Inaccessible/Broken: ${results.inaccessible.length}`); + console.log(`Errors: ${results.errors.length}`); + + if (results.cloudflare.length > 0) { + console.warn('\n' + '='.repeat(80)); + console.warn('CLOUDFLARE PROTECTED SITES (excluded from broken list):'); + console.warn('='.repeat(80)); + results.cloudflare.forEach((item, index) => { + console.warn( + `\n${index + 1}. ${item.pluginInfo.name || 'Unknown'} (${item.pluginInfo.id})`, + ); + console.warn(` URL: ${item.url}`); + console.warn(` Language: ${item.pluginInfo.lang || 'N/A'}`); + }); + } + + if (results.inaccessible.length > 0 || results.errors.length > 0) { + console.log('\n' + '='.repeat(80)); + console.log('INACCESSIBLE OR BROKEN SITES:'); + console.log('='.repeat(80)); + + const allFailed = [...results.inaccessible, ...results.errors]; + allFailed.forEach((item, index) => { + console.log( + `\n${index + 1}. ${item.pluginInfo.name || 'Unknown'} (${item.pluginInfo.id})`, + ); + console.log(` URL: ${item.url}`); + console.log(` Language: ${item.pluginInfo.lang || 'N/A'}`); + if (item.statusCode) { + console.log(` Status Code: ${item.statusCode}`); + } + if (item.reason) { + console.log(` Reason: ${item.reason}`); + } + if (item.error) { + console.log(` Error: ${item.error}`); + } + }); + + const reportFile = path.join(__dirname, '..', 'broken-sites-report.json'); + fs.writeFileSync( + reportFile, + JSON.stringify( + { + timestamp: new Date().toISOString(), + total: totalSites, + accessible: results.accessible.length, + cloudflare: results.cloudflare.length, + inaccessible: results.inaccessible.length, + errors: results.errors.length, + brokenSites: allFailed.map(item => ({ + id: item.pluginInfo.id, + name: item.pluginInfo.name, + url: item.url, + lang: item.pluginInfo.lang, + statusCode: item.statusCode, + reason: item.reason, + error: item.error, + })), + cloudflareSites: results.cloudflare.map(item => ({ + id: item.pluginInfo.id, + name: item.pluginInfo.name, + url: item.url, + lang: item.pluginInfo.lang, + statusCode: item.statusCode, + reason: item.reason, + })), + }, + null, + 2, + ), + ); + + console.log(`\n\nDetailed report saved to: ${reportFile}`); + } else { + console.log('\n✓ All sites are accessible!'); + } + } catch (error) { + console.error('Error:', error.message); + process.exit(1); + } +} + +main().catch(error => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/scripts/download-plugin-icons.js b/scripts/download-plugin-icons.js new file mode 100644 index 000000000..15b116c37 --- /dev/null +++ b/scripts/download-plugin-icons.js @@ -0,0 +1,163 @@ +// /* global Buffer */ +import * as fs from 'fs'; +import * as path from 'path'; +import sizeOf from 'image-size'; + +const size = 96; +const minSize = 16; + +const skip = new Set([ + //custom icons + 'FWK.US', + 'LeafStudio', + 'RNRF', + 'ReN', + 'WTRLAB', + 'azora', + 'coralboutique', + 'crimsonscrolls', + 'daonovel', + 'dragonholic', + 'dragontea', + 'foxaholic', + 'kiniga', + 'lightnovelpubvip', + 'moonlightnovel', + 'mtl-novel', + 'mysticalmerries', + 'novelTL', + 'novelsparadise', + 'prizmatranslation', + 'requiemtls', + 'sektenovel', + 'sonicmtl', + 'translatinotaku', + 'universalnovel', + 'warriorlegendtrad', + 'wuxialnscantrad', + 'wuxiaworld.site', +]); + +const folder = path.join('public', 'static'); + +const used = new Set([ + path.join(folder, 'coverNotAvailable.webp'), + path.join(folder, 'siteNotAvailable.png'), +]); + +const notAvailableImage = fs.readFileSync( + path.join(folder, 'siteNotAvailable.png'), +); + +(async () => { + console.log('Loading plugins.json ⌛'); + const plugin_path = path.join('.dist', 'plugins.json'); + if (!fs.existsSync(plugin_path)) { + console.log( + '❌', + plugin_path, + 'not found (run "npm run build:manifest" first)', + ); + return; + } + const plugins = JSON.parse(fs.readFileSync(plugin_path, 'utf-8')); + + console.log('\nDownloading icons ⌛'); + let language; + for (let plugin in plugins) { + const { id, name, site, iconUrl, lang, customJS, customCSS } = + plugins[plugin]; + const icon = iconUrl && path.join(folder, iconUrl.split('/static/')[1]); + + if (language !== lang) { + language = lang; + console.log( + ` ${language} ` + .padStart(Math.floor((language.length + 32) / 2), '=') + .padEnd(30, '='), + ); + } + + try { + if (customJS) { + used.add(path.join(folder, customJS.split('/static/')[1])); + } + if (customCSS) { + used.add(path.join(folder, customCSS.split('/static/')[1])); + } + if (icon) used.add(icon); + if (!skip.has(id) && icon && site) { + const image = await fetch( + `https://www.google.com/s2/favicons?domain=${site}&sz=${size}&type=png`, + ) + .then(res => res.arrayBuffer()) + .then(res => Buffer.from(res)); + + if (Buffer.compare(image, notAvailableImage) === 0) { + console.log( + ' ', + name.padEnd(26), + `(${id})`.padEnd(20), + 'Is site down?', + '\r❌', + ); + continue; + } + + const imageSize = sizeOf(image); + const exist = fs.existsSync(icon); + + if (!exist) { + const dir = path.dirname(icon); + fs.mkdirSync(dir, { recursive: true }); + } + + if ( + ((imageSize?.width || size) > minSize && + (imageSize?.height || size) > minSize) || + !exist + ) { + fs.writeFileSync(icon, image); + console.log(' ', name.padEnd(26), `(${id})`, '\r✅'); + } else { + console.log( + ' ', + name.padEnd(26), + `(${id})`.padEnd(20), + 'Low quality', + '\r🔄', + ); + } + } else { + console.log(' ', `Skipping ${name}`.padEnd(26), `(${id})`, '\r🔄'); + } + } catch (err) { + console.log( + ' ', + name.padEnd(26), + `(${id})`.padEnd(20), + err instanceof Error ? err.constructor.name : typeof err, + '\r❌', + ); + console.log(err); + await new Promise(resolve => setTimeout(resolve, 2500)); + } + } + console.log('\nDeleting unused icons ⌛'); + + fileList(folder).forEach(path => { + if (!used.has(path)) { + console.log('🗑️', path); + fs.rmSync(path, { force: true }); + } + }); + console.log('\nDone ✅'); +})(); + +function fileList(dir) { + return fs.readdirSync(dir).reduce((list, file) => { + const name = path.join(dir, file); + const isDir = fs.statSync(name).isDirectory(); + return list.concat(isDir ? fileList(name) : [name]); + }, []); +} diff --git a/scripts/generate-plugin-index.js b/scripts/generate-plugin-index.js new file mode 100644 index 000000000..7f19c22b9 --- /dev/null +++ b/scripts/generate-plugin-index.js @@ -0,0 +1,28 @@ +import fs from 'fs'; +import { execSync } from 'child_process'; + +let content = `import { Plugin } from '@/types/plugin';\n`; +let pluginCounter = 0; +const PLUGIN_DIR = 'plugins'; + +fs.readdirSync(PLUGIN_DIR) + .filter(f => !f.includes('index') && f !== 'multisrc') + .forEach(langName => { + const LANG_DIR = PLUGIN_DIR + '/' + langName; + fs.readdirSync(LANG_DIR) + .filter(f => !f.includes('broken') && !f.startsWith('.')) + .forEach(pluginName => { + content += `import p_${pluginCounter} from '@plugins/${langName}/${pluginName.replace(/\.ts$/, '')}';\n`; + pluginCounter += 1; + }); + }); + +content += `\nconst PLUGINS: Plugin.PluginBase[] = [${Array(pluginCounter) + .fill() + .map((v, index) => 'p_' + index.toString()) + .join(', ')}];\nexport default PLUGINS`; + +const outputPath = './plugins/index.ts'; +fs.writeFileSync(outputPath, content, { encoding: 'utf-8' }); + +execSync(`npx prettier --write ${outputPath}`, { stdio: 'inherit' }); diff --git a/scripts/languages.js b/scripts/languages.js new file mode 100644 index 000000000..2892b5935 --- /dev/null +++ b/scripts/languages.js @@ -0,0 +1,18 @@ +export default { + Arabic: '‎العربية', + Chinese: '中文, 汉语, 漢語', + English: 'English', + French: 'Français', + Indonesian: 'Bahasa Indonesia', + Japanese: '日本語', + Korean: '조선말, 한국어', + Polish: 'Polski', + Portuguese: 'Português', + Russian: 'Русский', + Spanish: 'Español', + Thai: 'ไทย', + Turkish: 'Türkçe', + Ukrainian: 'Українська', + Vietnamese: 'Tiếng Việt', + Multi: 'Multi', +}; diff --git a/scripts/publish-plugins.ps1 b/scripts/publish-plugins.ps1 new file mode 100644 index 000000000..bf58acafa --- /dev/null +++ b/scripts/publish-plugins.ps1 @@ -0,0 +1,42 @@ +$current=$(git rev-parse --abbrev-ref HEAD) +$version=$(node -e "console.log(require('./package.json').version);") +$dist="plugins/v$($version)" + +echo "Publishing plugins: $current -> $dist (v$version)" + +$exists=$(git show-ref refs/heads/$dist) + +if ($exists){ + git branch -D $dist +} + +git checkout --orphan $dist + +if(-Not $?){ + echo "❌ ERROR: Failed to create branch $dist" + exit 1 +} + +git reset +rm -r -fo .js +npm run clean:multisrc +npm run build:multisrc +echo "Compiling TypeScript..." +npx tsc --project tsconfig.production.json +npm run build:manifest + +if (-not (Test-Path .dist) -or -not (Get-ChildItem -Path .dist -Force)) { + echo "❌ ERROR: Manifest generation failed - .dist is missing or empty" + exit 1 +} + +# Copy plugins to legacy path (.js/src/plugins) for backward compatibility +echo "Copying .js/plugins -> .js/src/plugins" +New-Item -ItemType Directory -Force -Path .js/src | Out-Null +Copy-Item -Path .js/plugins -Destination .js/src/plugins -Recurse -Force +git add -f public/static .dist .js/src/plugins total.svg +git commit -m "chore: Publish Plugins" +git push -f origin $dist +git checkout -f $current +echo "✅ Published to $dist" + diff --git a/scripts/publish-plugins.sh b/scripts/publish-plugins.sh new file mode 100755 index 000000000..fba363c69 --- /dev/null +++ b/scripts/publish-plugins.sh @@ -0,0 +1,103 @@ +#!/bin/bash + +if [ -z "$GITHUB_STEP_SUMMARY" ]; then + GITHUB_STEP_SUMMARY="/dev/stdout" +fi + +current=`git rev-parse --abbrev-ref HEAD` +version=`node -e "console.log(require('./package.json').version);"` +dist="plugins/v$version" + +echo "Publishing plugins: $current -> $dist (v$version)" + +if [[ "$1" == "--all-branches" ]]; then + rm -rf .dist .js + git fetch --all + branches=$(git branch -r | grep -v '\->') + for branch in $branches; do + # Skip branches with different publish-plugins.sh version + if ! diff scripts/publish-plugins.sh <(git show "$branch:scripts/publish-plugins.sh") >/dev/null; then + echo "⚠️ Skipping $branch (script version mismatch)" + continue + fi + echo "::group::Branch $branch" + echo "Processing: $branch" + git stash push -a -- .dist .js + git checkout -f $branch + exists=`git show-ref refs/heads/$dist` + if [ -n "$exists" ]; then + git branch -D $dist + fi + git stash pop + npm run clean:multisrc + npm run build:multisrc + echo "Compiling TypeScript..." + npx tsc --project tsconfig.production.json + echo "# $branch" >> $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 + fi + echo "✅ Done: $branch" + echo "::endgroup::" + done + echo + echo "::group::Publish All Branches" + echo "Publishing combined plugins..." + git checkout --orphan $dist + if [ $? -eq 1 ]; then + echo "❌ ERROR: Failed to create branch $dist" + echo "::endgroup::" + exit 1 + fi + git reset + # Copy plugins to legacy path (.js/src/plugins) for backward compatibility + echo "Copying .js/plugins -> .js/src/plugins" + mkdir -p .js/src + cp -r .js/plugins .js/src/plugins + git add -f public/static .dist .js/src/plugins total.svg + git commit -m "chore: Publish Plugins From All Branches" + git push -f origin $dist + git checkout -f $branch + echo "✅ Published all branches to $dist" + echo "::endgroup::" + exit 0 +fi + +exists=`git show-ref refs/heads/$dist` + +if [ -n "$exists" ]; then + git branch -D $dist +fi + +git checkout --orphan $dist 2>&1 + +if [ $? -eq 1 ]; then + echo "❌ ERROR: Failed to create branch $dist" + exit 1 +fi + +git reset +rm -rf .js +npm run clean:multisrc +npm run build:multisrc +echo "Compiling TypeScript..." +npx tsc --project tsconfig.production.json +npm run build:manifest + +if [ ! -d ".dist" ] || [ -z "$(ls -A .dist)" ]; then + echo "❌ ERROR: Manifest generation failed - .dist is missing or empty" + exit 1 +fi + +# Copy plugins to legacy path (.js/src/plugins) for backward compatibility +echo "Copying .js/plugins -> .js/src/plugins" +mkdir -p .js/src +cp -r .js/plugins .js/src/plugins +git add -f public/static .dist .js/src/plugins total.svg +git commit -m "chore: Publish Plugins" +git push -f origin $dist 2>&1 +git checkout -f $current 2>&1 +echo "✅ Published to $dist" + diff --git a/scripts/terser.js b/scripts/terser.js new file mode 100644 index 000000000..306bf1028 --- /dev/null +++ b/scripts/terser.js @@ -0,0 +1,21 @@ +import { minify_sync } from 'terser'; +import fs from 'fs'; + +const config = { + compress: { + arrows: false, + }, + mangle: {}, + ecma: 5, // specify one of: 5, 2015, 2016, etc. + enclose: false, // or specify true, or "args:values" + module: true, + toplevel: true, +}; + +const minify = function (path) { + const code = fs.readFileSync(path).toString(); + const result = minify_sync(code, config); + fs.writeFileSync(path, result.code); +}; + +export { minify }; diff --git a/server.js b/server.js deleted file mode 100644 index f9ef507be..000000000 --- a/server.js +++ /dev/null @@ -1,27 +0,0 @@ -const express = require("express"); - -const app = express(); - -app.use( - express.json({ - extended: false, - }) -); - -app.get("/", (req, res) => - res.json({ - Author: "@rajarsheechatterjee", - Version: "1.0.0", - Github: "https://github.com/rajarsheechatterjee/lnreader-extensions", - }) -); - -// Box Novel -app.use("/api/1/", require("./src/en/boxnovel/boxnovel")); - -// ReadLightNovel -app.use("/api/2/", require("./src/en/readlightnovel/readlightnovel")); - -const PORT = process.env.PORT || 5000; - -app.listen(PORT, () => console.log(`Server started on port ${PORT}`)); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 000000000..f2e7996aa --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Toaster } from '@/components/ui/sonner'; +import Home from './pages/home'; +import { TooltipProvider } from './components/ui/tooltip'; +import { useTheme } from './hooks/useTheme'; + +function App() { + useTheme(); + + return ( +
+ + + + +
+ ); +} + +export default App; diff --git a/src/components/filters/checkbox-filter.tsx b/src/components/filters/checkbox-filter.tsx new file mode 100644 index 000000000..9d9c0f48f --- /dev/null +++ b/src/components/filters/checkbox-filter.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Label } from '@/components/ui/label'; +import { Checkbox } from '@/components/ui/checkbox'; +import { FilterTypes } from '@libs/filterInputs'; +import type { Filter } from '@libs/filterInputs'; + +type CheckboxFilterProps = { + filter: { + key: string; + filter: Filter; + }; + value: string[]; + set: (value: string[]) => void; +}; + +export function CheckboxFilter({ filter, value, set }: CheckboxFilterProps) { + const toggleOption = (optionValue: string) => { + if (value.includes(optionValue)) { + set(value.filter(v => v !== optionValue)); + } else { + set([...value, optionValue]); + } + }; + + return ( +
+ +
+ {filter.filter.options.map(option => ( +
+ toggleOption(option.value)} + /> + +
+ ))} +
+
+ ); +} diff --git a/src/components/filters/excludable-checkbox-filter.tsx b/src/components/filters/excludable-checkbox-filter.tsx new file mode 100644 index 000000000..8eeffc8c2 --- /dev/null +++ b/src/components/filters/excludable-checkbox-filter.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; +import { FilterTypes } from '@libs/filterInputs'; +import type { Filter } from '@libs/filterInputs'; + +type ExcludableCheckboxFilterProps = { + filter: { + key: string; + filter: Filter; + }; + value: { + include?: string[]; + exclude?: string[]; + }; + set: (value: { include?: string[]; exclude?: string[] }) => void; +}; + +type CheckState = 'unchecked' | 'included' | 'excluded'; + +export function ExcludableCheckboxFilter({ + filter, + value, + set, +}: ExcludableCheckboxFilterProps) { + const getState = (optionValue: string): CheckState => { + if (value.include?.includes(optionValue)) return 'included'; + if (value.exclude?.includes(optionValue)) return 'excluded'; + return 'unchecked'; + }; + + const toggleOption = (optionValue: string) => { + const currentState = getState(optionValue); + let newInclude = value.include || []; + let newExclude = value.exclude || []; + + switch (currentState) { + case 'unchecked': + // Unchecked -> Included + newInclude = [...newInclude, optionValue]; + break; + case 'included': + // Included -> Excluded + newInclude = newInclude.filter(v => v !== optionValue); + newExclude = [...newExclude, optionValue]; + break; + case 'excluded': + // Excluded -> Unchecked + newExclude = newExclude.filter(v => v !== optionValue); + break; + } + + set({ include: newInclude, exclude: newExclude }); + }; + + return ( +
+ +
+ {filter.filter.options.map(option => { + const state = getState(option.value); + return ( + + ); + })} +
+

+ Click once to include, twice to exclude, three times to reset +

+
+ ); +} diff --git a/src/components/filters/filters-sheet.tsx b/src/components/filters/filters-sheet.tsx new file mode 100644 index 000000000..8500fbd69 --- /dev/null +++ b/src/components/filters/filters-sheet.tsx @@ -0,0 +1,226 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { + Sheet, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { + AnyFilterValue, + Filters, + FilterToValues, + FilterTypes, +} from '@libs/filterInputs'; +import { PickerFilter } from './picker-filter'; +import { SwitchFilter } from './switch-filter'; +import { TextFilter } from './text-filter'; +import { CheckboxFilter } from './checkbox-filter'; +import { ExcludableCheckboxFilter } from './excludable-checkbox-filter'; +import { RotateCcw } from 'lucide-react'; + +const renderFilters = ( + filters: Filters | undefined, + values: FilterToValues | undefined, + set: (key: string, v: AnyFilterValue) => void, +): React.ReactNode => { + const isValueCorrectType = ( + o: AnyFilterValue, + f: T, + ): o is T => { + const checkIfIsCorrectObjectType = (o: AnyFilterValue, f: T): o is T => { + const areArrays = Array.isArray(o) && Array.isArray(f); + const areObjects = typeof o === 'object' && typeof f === 'object'; + return areArrays || areObjects; + }; + return typeof o === typeof f || checkIfIsCorrectObjectType(o, f); + }; + + if (!filters || !values) return null; + + return ( + <> + {Object.entries(filters).map(([key, filter]) => { + if (!(key in values)) { + console.error(`No filter value for ${key} in filter values!`); + return null; + } + switch (filter.type) { + case FilterTypes.Picker: { + const value = values[key].value; + if (!isValueCorrectType(value, filter.value)) { + console.error( + `FilterValue for filter [${key}] has a wrong type!`, + ); + return null; + } + return ( + set(key, newValue)} + /> + ); + } + case FilterTypes.Switch: { + const value = values[key].value; + if (!isValueCorrectType(value, filter.value)) { + console.error( + `FilterValue for filter [${key}] has a wrong type!`, + ); + return null; + } + return ( + set(key, newValue)} + /> + ); + } + case FilterTypes.TextInput: { + const value = values[key].value; + if (!isValueCorrectType(value, filter.value)) { + console.error( + `FilterValue for filter [${key}] has a wrong type!`, + ); + return null; + } + return ( + set(key, newValue)} + value={value} + key={`text_filter_${key}`} + /> + ); + } + case FilterTypes.CheckboxGroup: { + const value = values[key].value; + if (!isValueCorrectType(value, filter.value)) { + console.error( + `FilterValue for filter [${key}] has a wrong type!`, + ); + return null; + } + return ( + set(key, newValue)} + value={value} + key={`checkbox_filter_${key}`} + /> + ); + } + case FilterTypes.ExcludableCheckboxGroup: { + const value = values[key].value; + if (!isValueCorrectType(value, filter.value)) { + console.error( + `FilterValue for filter [${key}] has a wrong type!`, + ); + return null; + } + return ( + set(key, newValue)} + value={value} + key={`xcheсkbox_filter_${key}`} + /> + ); + } + } + })} + + ); +}; + +type FiltersSheetProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + values: FilterToValues | undefined; + filters: Filters | undefined; + setValues: React.Dispatch< + React.SetStateAction | undefined> + >; + refetch: () => void; +}; + +export function FiltersSheet({ + open, + onOpenChange, + values, + setValues, + filters, + refetch, +}: FiltersSheetProps) { + const setFilterWithKey = (key: string, newValue: AnyFilterValue) => + setValues(fValues => + !fValues + ? fValues + : { + ...fValues, + [key]: { + ...fValues[key], + value: newValue, + }, + }, + ); + + const resetFilters = () => { + if (filters) { + const resetValues: FilterToValues = {}; + for (const fKey in filters) { + resetValues[fKey as keyof typeof resetValues] = { + type: filters[fKey].type, + value: filters[fKey].value, + }; + } + setValues(resetValues); + } + }; + + const handleApply = () => { + refetch(); + onOpenChange(false); + }; + + const filterElements = renderFilters(filters, values, setFilterWithKey); + + return ( + + + + Filters + + Customize your search with these filter options + + +
+ {filterElements || ( +

+ No filters available +

+ )} +
+ + + + +
+
+ ); +} diff --git a/src/components/filters/picker-filter.tsx b/src/components/filters/picker-filter.tsx new file mode 100644 index 000000000..381712ff8 --- /dev/null +++ b/src/components/filters/picker-filter.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { FilterTypes } from '@libs/filterInputs'; +import type { Filter } from '@libs/filterInputs'; + +type PickerFilterProps = { + filter: { + key: string; + filter: Filter; + }; + value: string; + set: (value: string) => void; +}; + +const EMPTY_VALUE_PLACEHOLDER = '__lnreader_empty__'; + +export function PickerFilter({ filter, value, set }: PickerFilterProps) { + // Map empty string to placeholder for Radix UI Select + const displayValue = value === '' ? EMPTY_VALUE_PLACEHOLDER : value; + + // Handle change and map placeholder back to empty string + const handleChange = (newValue: string) => { + set(newValue === EMPTY_VALUE_PLACEHOLDER ? '' : newValue); + }; + + return ( +
+ + +
+ ); +} diff --git a/src/components/filters/switch-filter.tsx b/src/components/filters/switch-filter.tsx new file mode 100644 index 000000000..3dbe0ff04 --- /dev/null +++ b/src/components/filters/switch-filter.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { FilterTypes } from '@libs/filterInputs'; +import type { Filter } from '@libs/filterInputs'; + +type SwitchFilterProps = { + filter: { + key: string; + filter: Filter; + }; + value: boolean; + set: (value: boolean) => void; +}; + +export function SwitchFilter({ filter, value, set }: SwitchFilterProps) { + return ( +
+ + +
+ ); +} diff --git a/src/components/filters/text-filter.tsx b/src/components/filters/text-filter.tsx new file mode 100644 index 000000000..710242d08 --- /dev/null +++ b/src/components/filters/text-filter.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import { FilterTypes } from '@libs/filterInputs'; +import type { Filter } from '@libs/filterInputs'; + +type TextFilterProps = { + filter: { + key: string; + filter: Filter; + }; + value: string; + set: (value: string) => void; +}; + +export function TextFilter({ filter, value, set }: TextFilterProps) { + return ( +
+ + set(e.target.value)} + placeholder={`Enter ${filter.filter.label.toLowerCase()}...`} + /> +
+ ); +} diff --git a/src/components/novel-card.tsx b/src/components/novel-card.tsx new file mode 100644 index 000000000..89221157c --- /dev/null +++ b/src/components/novel-card.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { Copy, ArrowRight } from 'lucide-react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { Plugin } from '@/types/plugin'; + +type NovelCardProps = { + novel: Plugin.NovelItem; + onParse: (path: string) => void; +}; + +export function NovelCard({ novel, onParse }: NovelCardProps) { + const handleCopyPath = () => { + navigator.clipboard.writeText(novel.path); + toast.success('Novel path copied!'); + }; + + return ( +
+
+ {novel.name} +
+
+

+ {novel.name} +

+
+ + + + + +

Copy path

+
+
+ + + + + +

Parse novel

+
+
+
+
+
+ ); +} diff --git a/src/components/parse-chapter.tsx b/src/components/parse-chapter.tsx new file mode 100644 index 000000000..8d7274eda --- /dev/null +++ b/src/components/parse-chapter.tsx @@ -0,0 +1,341 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { Copy, FileText, Code } from 'lucide-react'; +import { toast } from 'sonner'; + +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Switch } from '@/components/ui/switch'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useAppStore } from '@/store'; +import { usePluginCustomAssets } from '@/hooks/usePluginCustomAssets'; + +const ParseChapterSection = React.memo(function ParseChapterSection() { + const plugin = useAppStore(state => state.plugin); + const parseChapterPath = useAppStore(state => state.parseChapterPath); + const shouldAutoSubmitChapter = useAppStore( + state => state.shouldAutoSubmitChapter, + ); + const clearParseChapterPath = useAppStore( + state => state.clearParseChapterPath, + ); + const [chapterPath, setChapterPath] = useState(''); + const [chapterText, setChapterText] = useState(''); + const [loading, setLoading] = useState(false); + const [fetchError, setFetchError] = useState(''); + const [showRawHtml, setShowRawHtml] = useState(false); + const lastProcessedPath = useRef(); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setChapterPath(''); + setChapterText(''); + setFetchError(''); + setShowRawHtml(false); + } + + const { customCSSLoaded, customJSLoaded, customCSSError, customJSError } = + usePluginCustomAssets(plugin, chapterText); + + const fetchChapterByPath = async (path: string) => { + if (plugin && path.trim()) { + setLoading(true); + setFetchError(''); + try { + const result = await plugin.parseChapter(path); + setChapterText(result); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to fetch chapter'; + setFetchError(errorMessage); + console.error('Error parsing chapter:', error); + } finally { + setLoading(false); + } + } + }; + + const fetchChapter = async () => { + await fetchChapterByPath(chapterPath); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && chapterPath.trim()) { + fetchChapter(); + } + }; + + const copyToClipboard = (text?: string, label?: string) => { + if (text) { + navigator.clipboard.writeText(text); + toast.success(`${label || 'Text'} copied to clipboard!`); + } + }; + + // Handle pre-filled path from navigation + useEffect(() => { + if (parseChapterPath === lastProcessedPath.current) return; + lastProcessedPath.current = parseChapterPath; + + if (parseChapterPath) { + setChapterPath(parseChapterPath); + + if (shouldAutoSubmitChapter && plugin) { + fetchChapterByPath(parseChapterPath); + } + + clearParseChapterPath(); + } + }, [ + parseChapterPath, + shouldAutoSubmitChapter, + plugin, + clearParseChapterPath, + ]); + + return ( +
+ +
+
+

+ Parse Chapter +

+

+ {plugin + ? 'Enter a chapter path to fetch content' + : 'Select a plugin to parse chapters'} +

+ {plugin && (plugin.customCSS || plugin.customJS) && ( +
+ + Available: + + {plugin.customCSS && ( + + Custom CSS + + )} + {plugin.customJS && ( + + Custom JS + + )} +
+ )} +
+
+ +
+ setChapterPath(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1" + disabled={!plugin} + /> + +
+ + {fetchError && ( +
+

{fetchError}

+
+ )} + + {loading && !chapterText ? ( +
+
+
+ + +
+
+ + +
+
+
+ +
+ + + + + + +
+
+
+ ) : !chapterText ? ( +
+
+ +
+

+ {plugin ? 'Ready to parse' : 'No plugin selected'} +

+

+ {plugin + ? 'Enter a chapter path in the field above and click "Fetch" to load the chapter content.' + : 'Please select a plugin from the sidebar to get started.'} +

+
+ ) : chapterText ? ( +
+
+
+

+ Chapter Content +

+

+ {chapterPath} +

+
+
+
+ + + Raw HTML + + +
+ + + + + +

Copy chapter path to clipboard

+
+
+ + + + + +

Copy chapter text to clipboard

+
+
+
+
+ +
+
+

+ {showRawHtml ? 'RAW HTML' : 'CHAPTER CONTENT'} ( + {chapterText.length} characters) +

+
+
+ {showRawHtml ? ( +
+                    {chapterText}
+                  
+ ) : ( +
+ )} +
+
+ +
+
+

+ Content loaded successfully +

+ {plugin?.customCSS && ( + + CSS:{' '} + {customCSSLoaded + ? '✓ Applied' + : customCSSError + ? '✗ Failed' + : '⋯ Loading'} + + )} + {plugin?.customJS && ( + + JS:{' '} + {customJSLoaded + ? '✓ Applied' + : customJSError + ? '✗ Failed' + : '⋯ Loading'} + + )} +
+ +
+
+ ) : null} + +
+ ); +}); + +export default ParseChapterSection; diff --git a/src/components/parse-novel.tsx b/src/components/parse-novel.tsx new file mode 100644 index 000000000..4a5dbe779 --- /dev/null +++ b/src/components/parse-novel.tsx @@ -0,0 +1,566 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { + BookOpen, + ChevronLeft, + ChevronRight, + Copy, + ArrowRight, + Download, +} from 'lucide-react'; +import { toast } from 'sonner'; + +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/components/ui/tooltip'; +import { useAppStore } from '@/store'; +import { Plugin } from '@/types/plugin'; +import { useEpubExport } from '@/hooks/useEpubExport'; + +type ParseNovelSectionProps = { + onNavigateToParseChapter?: () => void; +}; + +const ParseNovelSection = React.memo(function ParseNovelSection({ + onNavigateToParseChapter, +}: ParseNovelSectionProps) { + const plugin = useAppStore(state => state.plugin); + const parseNovelPath = useAppStore(state => state.parseNovelPath); + const shouldAutoSubmitNovel = useAppStore( + state => state.shouldAutoSubmitNovel, + ); + const clearParseNovelPath = useAppStore(state => state.clearParseNovelPath); + const setParseChapterPath = useAppStore(state => state.setParseChapterPath); + const [novelPath, setNovelPath] = useState(''); + const [sourceNovel, setSourceNovel] = useState< + (Plugin.SourceNovel & { totalPages?: number }) | undefined + >(); + const [chapters, setChapters] = useState([]); + const [loading, setLoading] = useState(false); + const [currentPage, setCurrentPage] = useState(1); + const [fetchError, setFetchError] = useState(''); + const lastProcessedPath = useRef(); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setNovelPath(''); + setSourceNovel(undefined); + setChapters([]); + setCurrentPage(1); + setFetchError(''); + } + + const { exportEpub, isExporting } = useEpubExport({ + plugin: plugin || null, + sourceNovel, + chapters, + novelPath, + }); + + const fetchNovelByPath = async (path: string) => { + if (plugin && path.trim()) { + setLoading(true); + setFetchError(''); + try { + const result = await plugin.parseNovel(path); + setSourceNovel(result); + setChapters(result.chapters || []); + setCurrentPage(1); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to fetch novel'; + setFetchError(errorMessage); + console.error('Error parsing novel:', error); + } finally { + setLoading(false); + } + } + }; + + const fetchNovel = async () => { + await fetchNovelByPath(novelPath); + }; + + const fetchPage = async (page: number) => { + if (plugin && novelPath && sourceNovel?.totalPages) { + setLoading(true); + setFetchError(''); + try { + const result = await (plugin as Plugin.PagePlugin).parsePage( + novelPath, + page.toString(), + ); + setChapters(result.chapters); + setCurrentPage(page); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to fetch page'; + setFetchError(errorMessage); + console.error('Error fetching page:', error); + } finally { + setLoading(false); + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && novelPath.trim()) { + fetchNovel(); + } + }; + + const copyToClipboard = (text?: string, label?: string) => { + if (text) { + navigator.clipboard.writeText(text); + toast.success(`${label || 'Text'} copied to clipboard!`); + } + }; + + const handleParseChapter = (path: string) => { + setParseChapterPath(path, true); + onNavigateToParseChapter?.(); + }; + + useEffect(() => { + if (parseNovelPath === lastProcessedPath.current) return; + lastProcessedPath.current = parseNovelPath; + + if (parseNovelPath) { + setNovelPath(parseNovelPath); + + if (shouldAutoSubmitNovel && plugin) { + fetchNovelByPath(parseNovelPath); + } + + clearParseNovelPath(); + } + }, [parseNovelPath, shouldAutoSubmitNovel, plugin, clearParseNovelPath]); + + const formatDate = (dateString?: string | null) => { + if (!dateString) return 'N/A'; + try { + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + }); + } catch { + return dateString; + } + }; + + return ( +
+ +
+
+

+ Parse Novel +

+

+ {plugin + ? 'Enter a novel path to fetch details' + : 'Select a plugin to parse novels'} +

+
+
+ +
+ setNovelPath(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1" + disabled={!plugin} + /> + +
+ + {fetchError && ( +
+

{fetchError}

+
+ )} + + {loading && !sourceNovel ? ( +
+
+
+
+ +
+ + + + +
+
+ +
+ +
+
+ ) : !sourceNovel ? ( +
+
+ +
+

+ {plugin ? 'Ready to parse' : 'No plugin selected'} +

+

+ {plugin + ? 'Enter a novel path in the field above and click "Fetch" to load detailed information, including chapters, metadata, and more.' + : 'Please select a plugin from the sidebar to get started.'} +

+
+ ) : sourceNovel ? ( +
+
+ {/* Novel Info */} +
+
+
+ copyToClipboard(sourceNovel.cover, 'Cover URL') + } + > + {sourceNovel.name} +
+
+

+ {sourceNovel.name} +

+
+ {sourceNovel.status && ( +
+

+ Status +

+ + {sourceNovel.status} + +
+ )} + {sourceNovel.author && ( +
+

+ Author +

+

+ {sourceNovel.author} +

+
+ )} + {sourceNovel.artist && ( +
+

+ Artist +

+

+ {sourceNovel.artist} +

+
+ )} + {sourceNovel.rating && ( +
+

+ Rating +

+

+ {sourceNovel.rating.toFixed(1)} / 5.0 +

+
+ )} +
+
+ + + + + +

Copy novel path to clipboard

+
+
+ + + + + +

+ {isExporting + ? 'Exporting chapters to EPUB...' + : 'Export all chapters as EPUB file'} +

+
+
+
+
+
+ + {sourceNovel.genres && ( +
+

+ Genres +

+
+ {sourceNovel.genres.split(/,\s*/).map((genre, index) => ( + + {genre} + + ))} +
+
+ )} + + {sourceNovel.summary && ( +
+

+ Summary +

+

+ {sourceNovel.summary} +

+
+ )} +
+ + {/* Sidebar Info */} +
+

Metadata

+
+
+

+ Total Chapters +

+

+ {chapters.length} +

+
+ {sourceNovel.totalPages && ( +
+

+ Total Pages +

+

+ {sourceNovel.totalPages} +

+
+ )} + {chapters.length > 0 && chapters[0].releaseTime && ( +
+

+ First Chapter +

+

+ {formatDate(chapters[0].releaseTime)} +

+
+ )} + {chapters.length > 0 && + chapters[chapters.length - 1].releaseTime && ( +
+

+ Last Updated +

+

+ {formatDate( + chapters[chapters.length - 1].releaseTime, + )} +

+
+ )} +
+
+
+ + {/* Chapters Table */} + {(chapters.length > 0 || sourceNovel.totalPages) && ( +
+
+

+ Chapters ({chapters.length}) +

+ {sourceNovel.totalPages && ( +
+ {sourceNovel.totalPages > 1 && ( + <> + + + Page {currentPage} of {sourceNovel.totalPages} + + + + )} + +
+ )} +
+
+ + + + + + + + {chapters.some(ch => ch.chapterNumber) && ( + + )} + + + + {chapters.map((chapter, index) => ( + + + + + + {chapters.some(ch => ch.chapterNumber) && ( + + )} + + ))} + +
+ # + + Name + + Actions + + Release Time + + Chapter # +
+ {index} + + {chapter.name} + +
+ + + + + +

Copy chapter path

+
+
+ + + + + +

Open in Parse Chapter tab

+
+
+
+
+ {formatDate(chapter.releaseTime)} + + {chapter.chapterNumber || '-'} +
+
+
+ )} +
+ ) : null} +
+
+ ); +}); + +export default ParseNovelSection; diff --git a/src/components/plugin-header.tsx b/src/components/plugin-header.tsx new file mode 100644 index 000000000..55fbb92e7 --- /dev/null +++ b/src/components/plugin-header.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Moon, Sun } from 'lucide-react'; + +import { Plugin } from '@/types/plugin'; +import { Button } from '@/components/ui/button'; +import { useTheme } from '@/hooks/useTheme'; + +type PluginHeaderProps = { + selectedPlugin?: Plugin.PluginBase; +}; + +export default function PluginHeader({ selectedPlugin }: PluginHeaderProps) { + const { theme, setTheme } = useTheme(); + + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark'); + }; + + const isDark = theme === 'dark'; + + return ( +
+
+
+ + 読 + +
+

+ Plugin Playground +

+

+ {selectedPlugin?.name} +

+
+
+ +
+
+ ); +} diff --git a/src/components/popular-novels.tsx b/src/components/popular-novels.tsx new file mode 100644 index 000000000..6dd2994a5 --- /dev/null +++ b/src/components/popular-novels.tsx @@ -0,0 +1,227 @@ +import React, { useState } from 'react'; +import { Filter, BookOpen } from 'lucide-react'; + +import { FiltersSheet } from '@/components/filters/filters-sheet'; +import { NovelCard } from '@/components/novel-card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useAppStore } from '@/store'; +import { FilterToValues, Filters } from '@libs/filterInputs'; +import { Plugin } from '@/types/plugin'; + +type PopularNovelsSectionProps = { + onNavigateToParseNovel?: () => void; +}; + +const PopularNovelsSection = React.memo(function PopularNovelsSection({ + onNavigateToParseNovel, +}: PopularNovelsSectionProps) { + const plugin = useAppStore(state => state.plugin); + const setParseNovelPath = useAppStore(state => state.setParseNovelPath); + const [novels, setNovels] = useState([]); + const [loading, setLoading] = useState(false); + const [currentIndex, setCurrentIndex] = useState(0); + const [maxIndex, setMaxIndex] = useState(0); + const [isLatest, setIsLatest] = useState(true); + const [filtersOpen, setFiltersOpen] = useState(false); + const [filterValues, setFilterValues] = useState< + FilterToValues | undefined + >(); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setCurrentIndex(0); + setMaxIndex(0); + setNovels([]); + + if (plugin?.filters) { + const filters: FilterToValues = {}; + for (const fKey in plugin.filters) { + filters[fKey as keyof typeof filters] = { + type: plugin.filters[fKey].type, + value: plugin.filters[fKey].value, + }; + } + setFilterValues(filters); + } else { + setFilterValues(undefined); + } + } + + const fetchNovelsByIndex = async ( + index: number, + latestOverride?: boolean, + ) => { + if (plugin && index) { + setLoading(true); + try { + const fetchedNovels = await plugin.popularNovels(index, { + filters: filterValues || {}, + showLatestNovels: latestOverride ?? isLatest, + }); + if (fetchedNovels.length !== 0) { + setCurrentIndex(index); + if (index > maxIndex) { + setMaxIndex(index); + } + setNovels(fetchedNovels); + } + } catch (error) { + console.error('Error fetching novels:', error); + } finally { + setLoading(false); + } + } + }; + + const handleIsLatestChange = (latest: boolean) => { + if (isLatest === latest) return; + setIsLatest(latest); + setCurrentIndex(0); + setMaxIndex(0); + setNovels([]); + }; + + const handleParseNovel = (path: string) => { + setParseNovelPath(path, true); + onNavigateToParseNovel?.(); + }; + + return ( +
+ +
+
+

+ Popular Novels +

+

+ {plugin + ? `Browse ${isLatest ? 'latest' : 'popular'} novels` + : 'Select a plugin to browse novels'} +

+
+
+ + + +
+
+ +
+ {['Latest', 'Popular'].map(option => ( + handleIsLatestChange(option === 'Latest')} + > + {option} + + ))} + {currentIndex > 0 && ( +
+ Page + { + const page = parseInt(e.target.value); + if (page > 0 && page <= maxIndex) { + fetchNovelsByIndex(page); + } + }} + className="w-16 h-7 text-center text-xs" + disabled={loading} + /> + + of {maxIndex}+ + +
+ )} +
+ + {loading ? ( +
+ {Array.from({ length: 12 }).map((_, index) => ( +
+ + + +
+ + +
+
+ ))} +
+ ) : novels.length === 0 ? ( +
+
+ +
+

+ {plugin ? 'No novels to display' : 'No plugin selected'} +

+

+ {plugin + ? 'Click the "Fetch" button above to load the latest or popular novels from this source.' + : 'Please select a plugin from the sidebar to get started.'} +

+
+ ) : ( +
+ {novels.map((novel, index) => ( + + ))} +
+ )} +
+ + fetchNovelsByIndex(1)} + /> +
+ ); +}); + +export default PopularNovelsSection; diff --git a/src/components/search-novels.tsx b/src/components/search-novels.tsx new file mode 100644 index 000000000..4245b2e05 --- /dev/null +++ b/src/components/search-novels.tsx @@ -0,0 +1,166 @@ +import React, { useState } from 'react'; +import { Search as SearchIcon } from 'lucide-react'; + +import { NovelCard } from '@/components/novel-card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { useAppStore } from '@/store'; +import { Plugin } from '@/types/plugin'; + +type SearchNovelsSectionProps = { + onNavigateToParseNovel?: () => void; +}; + +const SearchNovelsSection = React.memo(function SearchNovelsSection({ + onNavigateToParseNovel, +}: SearchNovelsSectionProps) { + const plugin = useAppStore(state => state.plugin); + const setParseNovelPath = useAppStore(state => state.setParseNovelPath); + const [searchTerm, setSearchTerm] = useState(''); + const [currentPage, setCurrentPage] = useState(1); + const [novels, setNovels] = useState([]); + const [loading, setLoading] = useState(false); + const [fetchError, setFetchError] = useState(''); + const [prevPluginId, setPrevPluginId] = useState(); + + if (plugin?.id !== prevPluginId) { + setPrevPluginId(plugin?.id); + setCurrentPage(1); + setNovels([]); + setSearchTerm(''); + setFetchError(''); + } + + const fetchNovels = async (page: number) => { + if (plugin && searchTerm.trim()) { + setLoading(true); + setFetchError(''); + try { + const results = await plugin.searchNovels(searchTerm, page); + setNovels(results); + setCurrentPage(page); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to fetch novels'; + setFetchError(errorMessage); + console.error('Error searching novels:', error); + } finally { + setLoading(false); + } + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && searchTerm.trim()) { + fetchNovels(1); + } + }; + + const handleParseNovel = (path: string) => { + setParseNovelPath(path, true); + onNavigateToParseNovel?.(); + }; + + return ( +
+ +
+
+

+ Search Novels +

+

+ {plugin + ? 'Search for novels by title or keywords' + : 'Select a plugin to search novels'} +

+
+ {currentPage > 1 && ( + Page {currentPage} + )} +
+ +
+ setSearchTerm(e.target.value)} + onKeyDown={handleKeyDown} + className="flex-1" + disabled={!plugin} + /> + + +
+ + {fetchError && ( +
+

{fetchError}

+
+ )} + + {loading ? ( +
+ {Array.from({ length: 12 }).map((_, index) => ( +
+ + + +
+ + +
+
+ ))} +
+ ) : novels.length === 0 ? ( +
+
+ +
+

+ {searchTerm + ? 'No results found' + : plugin + ? 'Ready to search' + : 'No plugin selected'} +

+

+ {searchTerm + ? 'Try adjusting your search term or check the spelling. Different sources may have different availability.' + : plugin + ? 'Enter a search term in the field above and click "Search" to find novels.' + : 'Please select a plugin from the sidebar to get started.'} +

+
+ ) : ( +
+ {novels.map((novel, index) => ( + + ))} +
+ )} +
+
+ ); +}); + +export default SearchNovelsSection; diff --git a/src/components/settings.tsx b/src/components/settings.tsx new file mode 100644 index 000000000..c90e28918 --- /dev/null +++ b/src/components/settings.tsx @@ -0,0 +1,213 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { CheckedState } from '@radix-ui/react-checkbox'; +import { Check } from 'lucide-react'; + +import { Card } from '@/components/ui/card'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import useDebounce from '@/hooks/useDebounce'; +import { FetchMode } from '@/types/types'; + +const FETCH_MODES = { + [FetchMode.PROXY]: 'Proxy', + [FetchMode.NODE_FETCH]: 'Node Fetch', + [FetchMode.CURL]: 'Curl', +}; + +const SettingsSection = React.memo(function SettingsSection() { + const [settings, setSettings] = useState({ + cookies: '', + fetchMode: FetchMode.PROXY, + useUserAgent: true as CheckedState, + }); + const [status, setStatus] = useState<'idle' | 'loading' | 'saved'>('idle'); + const init = useRef(false); + const lastSaved = useRef(null); + const debouncedCookies = useDebounce(settings.cookies, 500); + + useEffect(() => { + fetch('settings') + .then(res => res.json()) + .then(data => { + const loaded = { + cookies: data.cookies || '', + fetchMode: data.fetchMode ?? FetchMode.PROXY, + useUserAgent: data.useUserAgent ?? true, + }; + setSettings(loaded); + lastSaved.current = loaded; + init.current = true; + }) + .catch(console.error); + }, []); + + useEffect(() => { + if (!init.current || debouncedCookies !== settings.cookies) return; + const current = { ...settings, cookies: debouncedCookies }; + + if (JSON.stringify(lastSaved.current) === JSON.stringify(current)) return; + + setStatus('loading'); + fetch('settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(current), + }) + .then(() => { + lastSaved.current = current; + setStatus('saved'); + setTimeout(() => setStatus('idle'), 2000); + }) + .catch(console.error); + }, [ + debouncedCookies, + settings.fetchMode, + settings.useUserAgent, + settings.cookies, + ]); + + const update = ( + k: K, + v: (typeof settings)[K], + ) => setSettings(settings => ({ ...settings, [k]: v })); + + return ( +
+ + {status === 'saved' && ( +
+ Settings updated +
+ )} + +
+
+

Settings

+

+ Settings are automatically saved +

+
+ {status === 'loading' && ( +
Saving...
+ )} +
+ +
+
+
+ +
+ +
+ update('useUserAgent', v)} + /> + +
+
+
+ +
+ + update('cookies', e.target.value.trim())} + placeholder="Enter cookies (optional)..." + className="font-mono text-xs" + /> +

+ Additional cookies to send with requests (optional) +

+
+
+ +
+
+ + +

+ Select the method used to fetch data from sources +

+
+
+
+
+
+ ); +}); + +function Section({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+
+
+

+ {title} +

+
+
+
{children}
+
+ ); +} + +export default SettingsSection; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 000000000..bf7d44869 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +); + +type BadgeProps = React.ComponentPropsWithoutRef<'span'> & + VariantProps & { + asChild?: boolean; + }; + +const Badge = React.forwardRef( + ({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'span'; + + return ( + + ); + }, +); +Badge.displayName = 'Badge'; + +export { Badge, badgeVariants }; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 000000000..8542771e9 --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + }, +); + +type ButtonProps = React.ComponentPropsWithoutRef<'button'> & + VariantProps & { + asChild?: boolean; + }; + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); + }, +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx new file mode 100644 index 000000000..8d9dfd8d8 --- /dev/null +++ b/src/components/ui/card.tsx @@ -0,0 +1,111 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Card = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +Card.displayName = 'Card'; + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = 'CardHeader'; + +const CardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +CardTitle.displayName = 'CardTitle'; + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardDescription.displayName = 'CardDescription'; + +const CardAction = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardAction.displayName = 'CardAction'; + +const CardContent = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardContent.displayName = 'CardContent'; + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.ComponentProps<'div'> +>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = 'CardFooter'; + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +}; diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 000000000..b6ae45726 --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,32 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; +import { CheckIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 000000000..436111a4b --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 000000000..65fc3d6b6 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; + +import { cn } from '@/lib/utils'; + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 000000000..1ae483911 --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,173 @@ +import * as React from 'react'; +import * as SelectPrimitive from '@radix-ui/react-select'; +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + size?: 'sm' | 'default'; + } +>(({ className, size = 'default', children, ...props }, ref) => ( + + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, children, position = 'popper', align = 'center', ...props }, + ref, + ) => ( + + + + + {children} + + + + + ), +); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +export { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectLabel, + SelectScrollDownButton, + SelectScrollUpButton, + SelectSeparator, + SelectTrigger, + SelectValue, +}; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx new file mode 100644 index 000000000..28fd5d7b2 --- /dev/null +++ b/src/components/ui/sheet.tsx @@ -0,0 +1,131 @@ +import * as React from 'react'; +import * as SheetPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +const Sheet = SheetPrimitive.Root; + +const SheetTrigger = SheetPrimitive.Trigger; + +const SheetClose = SheetPrimitive.Close; + +const SheetPortal = SheetPrimitive.Portal; + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; + +type SheetContentProps = React.ComponentPropsWithoutRef< + typeof SheetPrimitive.Content +> & { + side?: 'top' | 'right' | 'bottom' | 'left'; +}; + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = 'right', className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +SheetContent.displayName = SheetPrimitive.Content.displayName; + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetHeader.displayName = 'SheetHeader'; + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +SheetFooter.displayName = 'SheetFooter'; + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetTitle.displayName = SheetPrimitive.Title.displayName; + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SheetDescription.displayName = SheetPrimitive.Description.displayName; + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +}; diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 000000000..d86b9a581 --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { cn } from '@/lib/utils'; + +const Skeleton = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + return ( +
+ ); +}); +Skeleton.displayName = 'Skeleton'; + +export { Skeleton }; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 000000000..96de4f509 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + CircleCheckIcon, + InfoIcon, + Loader2Icon, + OctagonXIcon, + TriangleAlertIcon, +} from 'lucide-react'; +import { Toaster as Sonner, type ToasterProps } from 'sonner'; + +const Toaster = ({ ...props }: ToasterProps) => { + return ( + , + info: , + warning: , + error: , + loading: , + }} + style={ + { + '--normal-bg': 'var(--popover)', + '--normal-text': 'var(--popover-foreground)', + '--normal-border': 'var(--border)', + '--border-radius': 'var(--radius)', + } as React.CSSProperties + } + {...props} + /> + ); +}; + +export { Toaster }; diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 000000000..019565695 --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from 'react'; +import * as SwitchPrimitives from '@radix-ui/react-switch'; + +import { cn } from '@/lib/utils'; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 000000000..2b567d14e --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import * as TabsPrimitive from '@radix-ui/react-tabs'; + +import { cn } from '@/lib/utils'; + +const Tabs = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Tabs.displayName = TabsPrimitive.Root.displayName; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx new file mode 100644 index 000000000..785f88245 --- /dev/null +++ b/src/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; + +import { cn } from '@/lib/utils'; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/src/en/boxnovel/boxnovel.js b/src/en/boxnovel/boxnovel.js deleted file mode 100644 index cc3eb4195..000000000 --- a/src/en/boxnovel/boxnovel.js +++ /dev/null @@ -1,235 +0,0 @@ -const express = require("express"); -const cheerio = require("cheerio"); -const request = require("request"); - -const router = express.Router(); - -const baseUrl = "https://boxnovel.com/novel"; - -const searchUrl = "https://boxnovel.com/"; - -// Top novels - -router.get("/novels/:pageNo/", (req, res) => { - orderBy = req.query.o; - pageNo = req.params.pageNo; - - url = `${baseUrl}/page/${pageNo}/?m_orderby=${orderBy}`; - - request(url, (err, response, body) => { - if (err) throw err; - - let novels = []; - - $ = cheerio.load(body); - - $(".page-item-detail").each(function (result) { - novelName = $(this).find("h5 > a").text(); - novelCover = $(this).find("img").attr("src"); - novelUrl = $(this) - .find("h5 > a") - .attr("href") - .replace(`${baseUrl}/`, ""); - novel = { - extensionId: 1, - novelName: novelName, - novelCover: novelCover, - novelUrl: novelUrl, - }; - - novels.push(novel); - }); - res.json(novels); - }); -}); - -// Novel - -router.get("/novel/:novelUrl", (req, res) => { - novelUrl = req.params.novelUrl; - url = `${baseUrl}/${novelUrl}/`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - let novel = {}; - - $(".post-title > h3 > span").remove(); - - novel.extensionId = 1; - - novel.sourceName = "BoxNovel"; - - novel.sourceUrl = url; - - novel.novelUrl = `${novelUrl}/`; - - novel.novelName = $(".post-title > h3") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - novel.novelCover = $(".summary_image > a > img").attr("src"); - - $(".post-content_item").each(function (result) { - detailName = $(this) - .find(".summary-heading > h5") - .text() - .replace(/[\t\n]/g, "") - .trim(); - detail = $(this) - .find(".summary-content") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - novel[detailName] = detail; - }); - - $(".description-summary > div.summary__content").find("em").remove(); - - novel.novelSummary = $( - ".description-summary > div.summary__content > div" - ) - .text() - .replace(/[\t\n]/g, ""); - - // if (novel.novelSummary === "") { - // novel.novelSummary = $(".c_000").text(); - // } - - let novelChapters = []; - - $(".wp-manga-chapter").each(function (result) { - chapterName = $(this) - .find("a") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - releaseDate = $(this) - .find("span") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - chapterUrl = $(this).find("a").attr("href").replace(url, ""); - - novelChapters.push({ chapterName, releaseDate, chapterUrl }); - }); - - novel.novelChapters = novelChapters.reverse(); - - res.json(novel); - }); -}); - -// Chapter - -router.get("/novel/:novelUrl/:chapterUrl", (req, res) => { - url = `${baseUrl}/${req.params.novelUrl}/${req.params.chapterUrl}`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - chapterNameLower = req.params.chapterUrl.replace("-", " "); - - chapterName = - chapterNameLower.charAt(0).toUpperCase() + - chapterNameLower.slice(1); - - chapterText = $(".reading-content").text(); - - let nextChapter = null; - - if ($(".nav-next").length) { - nextChapter = $(".nav-next") - .find("a") - .attr("href") - .replace(baseUrl + "/" + req.params.novelUrl + "/", ""); - } - - let prevChapter = null; - - if ($(".nav-previous").length) { - prevChapter = $(".nav-previous") - .find("a") - .attr("href") - .replace(baseUrl + "/" + req.params.novelUrl + "/", ""); - } - - chapter = { - extensionId: 1, - novelUrl: `${req.params.novelUrl}/`, - chapterUrl: `${req.params.chapterUrl}/`, - chapterName, - chapterText, - nextChapter, - prevChapter, - }; - - res.json(chapter); - }); -}); - -// Search - -router.get("/search/", (req, res) => { - searchTerm = req.query.s; - orderBy = req.query.o; - - url = `${searchUrl}?s=${searchTerm}&post_type=wp-manga&m_orderby=${orderBy}`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - let novels = []; - - $(".c-tabs-item__content").each(function (result) { - novelName = $(this).find("h4 > a").text(); - - // let novelDetails = {}; - - // $(this) - // .find(".post-content_item") - // .each(function (result) { - // detailName = $(this) - // .find(".summary-heading > h5") - // .text() - // .replace(/[\t\n]/g, "") - // .trim(); - // detail = $(this) - // .find(".summary-content") - // .text() - // .replace(/[\t\n]/g, "") - // .trim(); - - // novelDetails[detailName] = detail; - // }); - - novelCover = $(this).find("img").attr("src"); - - novelUrl = $(this) - .find("h4 > a") - .attr("href") - .replace(`${baseUrl}/`, ""); - - novels.push({ - extensionId: 1, - novelName, - novelCover, - novelUrl, - }); - }); - - res.json(novels); - }); -}); - -module.exports = router; diff --git a/src/en/readlightnovel/readlightnovel.js b/src/en/readlightnovel/readlightnovel.js deleted file mode 100644 index 9ecf2179a..000000000 --- a/src/en/readlightnovel/readlightnovel.js +++ /dev/null @@ -1,217 +0,0 @@ -const express = require("express"); -const cheerio = require("cheerio"); -const request = require("request"); - -const router = express.Router(); - -const baseUrl = "https://www.readlightnovel.org"; - -const searchUrl = "https://www.readlightnovel.org/detailed-search"; - -// Top Novels - -router.get("/novels/", (req, res) => { - // orderBy = req.query.o; - let novels = []; - - url = `${baseUrl}/top-novel?change_type=top_rated`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - $(".top-novel-block").each(function (result) { - novelName = $(this).find(".top-novel-header > h2 > a").text(); - novelCover = $(this).find("img").attr("src"); - novelUrl = $(this) - .find(".top-novel-header > h2 > a") - .attr("href") - .replace(`${baseUrl}/`, ""); - novel = { - extensionId: 2, - novelName: novelName, - novelCover: novelCover, - novelUrl: `${novelUrl}/`, - }; - - novels.push(novel); - }); - - res.json(novels); - }); -}); - -// Novel - -router.get("/novel/:novelUrl", (req, res) => { - novelUrl = req.params.novelUrl; - url = `${baseUrl}/${novelUrl}`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - let novel = {}; - - novel.extensionId = 2; - - novel.sourceName = "ReadLightNovel"; - - novel.sourceUrl = url; - - novel.novelUrl = `${novelUrl}/`; - - novel.novelName = $(".block-title > h1").text(); - - novel.novelCover = $(".novel-cover > a > img").attr("src"); - - $(".novel-detail-item").each(function (result) { - detailName = $(this) - .find(".novel-detail-header > h6") - .text() - .replace(/[\t\n]/g, "") - .trim(); - detail = $(this) - .find(".novel-detail-body") - .text() - // .replace(/[\t\n]/g, " ") - .trim(); - - novel[detailName] = detail; - }); - - novel.Alternative = novel["Alternative Names"]; - novel.novelSummary = novel.Description; - novel["Genre(s)"] = novel.Genre.replace(/[\t\n]/g, ", "); - novel.Release = novel.Year; - - delete novel["Alternative Names"]; - delete novel.Description; - delete novel.Genre; - delete novel.Year; - - let novelChapters = []; - - $(".tab-content") - .find("li") - .each(function (result) { - chapterName = $(this) - .find("a") - .text() - .replace(/[\t\n]/g, "") - .trim(); - - releaseDate = null; - // $(this) - // .find("span") - // .text() - // .replace(/[\t\n]/g, "") - // .trim(); - - chapterUrl = $(this) - .find("a") - .attr("href") - .replace(baseUrl, ""); - - novelChapters.push({ - chapterName, - releaseDate, - chapterUrl: chapterUrl.replace(`/${novelUrl}/`, ""), - }); - }); - - novel.novelChapters = novelChapters; - - res.json(novel); - }); -}); - -// Chapter - -router.get("/novel/:novelUrl/:chapterUrl/:volumeUrl?", (req, res) => { - req.params.volumeUrl - ? (optionalUrl = req.params.volumeUrl) - : (optionalUrl = ""); - - url = `${baseUrl}/${req.params.novelUrl}/${req.params.chapterUrl}/${optionalUrl}`; - - request(url, (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - $(".block-title > h1").find("a").remove(); - - chapterName = $(".block-title > h1").text().replace(" - ", ""); - - // $(".desc").find(".trinity-player-iframe-wrapper").remove(); - - chapterText = $(".desc").text(); - - let nextChapter = null; - - if ($("a.next.next-link").length) { - nextChapter = $("a.next.next-link") - .attr("href") - .replace(`${baseUrl}/${req.params.novelUrl}/`, ""); - } - - let prevChapter = null; - - if ($("a.prev.prev-link").length) { - prevChapter = $("a.prev.prev-link") - .attr("href") - .replace(`${baseUrl}/${req.params.novelUrl}/`, ""); - } - - chapter = { - extensionId: 2, - novelUrl: req.params.novelUrl, - chapterUrl: `${req.params.chapterUrl}/${optionalUrl}`, - chapterName, - chapterText, - nextChapter, - prevChapter, - }; - - res.json(chapter); - }); -}); - -router.get("/search/", (req, res) => { - searchTerm = req.query.s; - - request.post( - { url: searchUrl, form: { keyword: searchTerm, search: 1 } }, - (err, response, body) => { - if (err) throw err; - - $ = cheerio.load(body); - - let novels = []; - - $(".top-novel-block").each(function (result) { - novelName = $(this).find(".top-novel-header > h2 > a").text(); - novelCover = $(this).find("img").attr("src"); - novelUrl = $(this) - .find(".top-novel-header > h2 > a") - .attr("href") - .replace(`${baseUrl}/`, ""); - novel = { - extensionId: 2, - novelName: novelName, - novelCover: novelCover, - novelUrl: `${novelUrl}/`, - }; - - novels.push(novel); - }); - - res.json(novels); - } - ); -}); - -module.exports = router; diff --git a/src/en/wntl/icon.png b/src/en/wntl/icon.png new file mode 100644 index 000000000..da548d90f Binary files /dev/null and b/src/en/wntl/icon.png differ diff --git a/src/hooks/useDebounce.ts b/src/hooks/useDebounce.ts new file mode 100644 index 000000000..3c5468fb7 --- /dev/null +++ b/src/hooks/useDebounce.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react'; + +export default function useDebounce(text: string, delay: number) { + const [value, setValue] = useState(''); + + useEffect(() => { + const timerId = setTimeout(() => { + setValue(text); + }, delay); + return () => { + clearTimeout(timerId); + }; + }, [text, delay]); + return value; +} diff --git a/src/hooks/useEpubExport.ts b/src/hooks/useEpubExport.ts new file mode 100644 index 000000000..075390446 --- /dev/null +++ b/src/hooks/useEpubExport.ts @@ -0,0 +1,152 @@ +import { useState } from 'react'; +import { toast } from 'sonner'; +import { Plugin } from '@/types/plugin'; +import { createEpub, downloadBlob } from '@/lib/epub'; + +type UseEpubExportOptions = { + plugin: Plugin.PluginBase | null; + sourceNovel: (Plugin.SourceNovel & { totalPages?: number }) | undefined; + chapters: Plugin.ChapterItem[]; + novelPath: string; +}; + +export function useEpubExport({ + plugin, + sourceNovel, + chapters, + novelPath, +}: UseEpubExportOptions) { + const [isExporting, setIsExporting] = useState(false); + + const exportEpub = async () => { + if (!plugin || !sourceNovel || chapters.length === 0) { + toast.error('No novel or chapters available to export'); + return; + } + + setIsExporting(true); + const toastId = toast.loading('Starting EPUB export...', { + description: `Preparing to export ${chapters.length} chapters`, + }); + + try { + const allChapters: Plugin.ChapterItem[] = []; + + if (sourceNovel.totalPages && sourceNovel.totalPages > 1) { + toast.loading('Fetching all chapters...', { + id: toastId, + description: `Found ${sourceNovel.totalPages} pages`, + }); + + for (let page = 1; page <= sourceNovel.totalPages; page++) { + try { + const pageResult = await (plugin as Plugin.PagePlugin).parsePage( + novelPath, + page.toString(), + ); + allChapters.push(...pageResult.chapters); + + toast.loading('Fetching chapters...', { + id: toastId, + description: `Page ${page}/${sourceNovel.totalPages} - ${allChapters.length} chapters collected`, + }); + } catch (error) { + console.error(`Error fetching page ${page}:`, error); + } + } + } else { + allChapters.push(...chapters); + } + + if (allChapters.length === 0) { + toast.error('No chapters found to export', { id: toastId }); + setIsExporting(false); + return; + } + + toast.loading('Fetching chapter content...', { + id: toastId, + description: `0/${allChapters.length} chapters processed`, + }); + + type Chapter = { + title: string; + content: string; + path: string; + }; + const chapterContents: Chapter[] = []; + + for (let i = 0; i < allChapters.length; i++) { + const chapter = allChapters[i]; + try { + const content = await plugin.parseChapter(chapter.path); + chapterContents.push({ + title: chapter.name, + content: content || '

No content available

', + path: chapter.path, + }); + + const progress = Math.round(((i + 1) / allChapters.length) * 100); + toast.loading('Fetching chapter content...', { + id: toastId, + description: `${i + 1}/${allChapters.length} chapters processed (${progress}%)`, + }); + } catch (error) { + console.error(`Error fetching chapter ${i + 1}:`, error); + chapterContents.push({ + title: chapter.name, + content: `

Error: Failed to fetch chapter content

`, + path: chapter.path, + }); + } + } + + toast.loading('Generating EPUB file...', { + id: toastId, + description: 'Creating EPUB structure', + }); + + let coverUrl = sourceNovel.cover; + if (coverUrl && plugin.resolveUrl) { + coverUrl = plugin.resolveUrl(coverUrl, true); + } else if ( + coverUrl && + !coverUrl.startsWith('http://') && + !coverUrl.startsWith('https://') + ) { + coverUrl = coverUrl.startsWith('/') ? coverUrl : '/' + coverUrl; + } + + const epubBlob = await createEpub(chapterContents, { + title: sourceNovel.name, + author: sourceNovel.author, + description: sourceNovel.summary, + cover: coverUrl, + language: 'en', + }); + + const filename = `${sourceNovel.name.replace(/[^a-z0-9]/gi, '_')}.epub`; + downloadBlob(epubBlob, filename); + + toast.success('EPUB exported successfully!', { + id: toastId, + description: `Downloaded ${allChapters.length} chapters as ${filename}`, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : 'Failed to export EPUB'; + toast.error('Export failed', { + id: toastId, + description: errorMessage, + }); + console.error('Error exporting EPUB:', error); + } finally { + setIsExporting(false); + } + }; + + return { + exportEpub, + isExporting, + }; +} diff --git a/src/hooks/usePluginCustomAssets.ts b/src/hooks/usePluginCustomAssets.ts new file mode 100644 index 000000000..dedcd11e5 --- /dev/null +++ b/src/hooks/usePluginCustomAssets.ts @@ -0,0 +1,106 @@ +import { useEffect, useRef, useState } from 'react'; +import { Plugin } from '@/types/plugin'; + +type UsePluginCustomAssetsReturn = { + customCSSLoaded: boolean; + customJSLoaded: boolean; + customCSSError: boolean; + customJSError: boolean; +}; + +/** + * Custom hook to load and manage plugin custom CSS and JS assets + * @param plugin - The current plugin instance + * @param chapterText - The loaded chapter text (triggers asset loading) + * @returns Object containing loading states for CSS and JS + */ +export function usePluginCustomAssets( + plugin: Plugin.PluginBase | undefined, + chapterText: string, +): UsePluginCustomAssetsReturn { + const [customCSSLoaded, setCustomCSSLoaded] = useState(false); + const [customJSLoaded, setCustomJSLoaded] = useState(false); + const [customCSSError, setCustomCSSError] = useState(false); + const [customJSError, setCustomJSError] = useState(false); + const customStyleRef = useRef(null); + const customScriptRef = useRef(null); + + useEffect(() => { + // Clean up previous custom styles + if (customStyleRef.current) { + customStyleRef.current.remove(); + customStyleRef.current = null; + } + + setCustomCSSLoaded(false); + setCustomCSSError(false); + + if (plugin?.customCSS && chapterText) { + const styleElement = document.createElement('style'); + styleElement.id = 'plugin-custom-css'; + + fetch(`/public/static/${plugin.customCSS}`) + .then(response => response.text()) + .then(cssContent => { + styleElement.textContent = cssContent; + document.head.appendChild(styleElement); + customStyleRef.current = styleElement; + setCustomCSSLoaded(true); + }) + .catch(error => { + console.error('Error loading custom CSS:', error); + setCustomCSSError(true); + }); + } + + return () => { + if (customStyleRef.current) { + customStyleRef.current.remove(); + customStyleRef.current = null; + } + }; + }, [plugin?.customCSS, chapterText]); + + useEffect(() => { + if (customScriptRef.current) { + customScriptRef.current.remove(); + customScriptRef.current = null; + } + + setCustomJSLoaded(false); + setCustomJSError(false); + + if (plugin?.customJS && chapterText) { + const scriptElement = document.createElement('script'); + scriptElement.id = 'plugin-custom-js'; + scriptElement.src = `/public/static/${plugin.customJS}`; + + scriptElement.onload = () => { + console.log('Custom JS loaded successfully'); + setCustomJSLoaded(true); + }; + + scriptElement.onerror = error => { + console.error('Error loading custom JS:', error); + setCustomJSError(true); + }; + + document.head.appendChild(scriptElement); + customScriptRef.current = scriptElement; + } + + return () => { + if (customScriptRef.current) { + customScriptRef.current.remove(); + customScriptRef.current = null; + } + }; + }, [plugin?.customJS, chapterText]); + + return { + customCSSLoaded, + customJSLoaded, + customCSSError, + customJSError, + }; +} diff --git a/src/hooks/useTheme.ts b/src/hooks/useTheme.ts new file mode 100644 index 000000000..6afec6dac --- /dev/null +++ b/src/hooks/useTheme.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; +import { useAppStore } from '@/store'; + +export function useTheme() { + const theme = useAppStore(state => state.theme); + const setTheme = useAppStore(state => state.setTheme); + + useEffect(() => { + const root = document.documentElement; + if (theme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + }, [theme]); + + return { theme, setTheme }; +} diff --git a/src/index.css b/src/index.css new file mode 100644 index 000000000..c2ef8d35e --- /dev/null +++ b/src/index.css @@ -0,0 +1,136 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; +@import '@fontsource/geist-sans/index.css'; +@import '@fontsource/geist-mono/index.css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); + --accent-strong: #1d6378; + --accent-soft: rgba(43, 127, 150, 0.18); + --outline: rgba(45, 52, 64, 0.45); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); + --accent-strong: #2b7f96; + --accent-soft: rgba(43, 127, 150, 0.25); + --outline: rgba(43, 127, 150, 0.45); +} + +@theme inline { + --font-sans: 'Geist Sans', system-ui, -apple-system, sans-serif; + --font-mono: 'Geist Mono', ui-monospace, monospace; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --color-accent-strong: var(--accent-strong); + --color-accent-soft: var(--accent-soft); + --color-outline: var(--outline); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground font-sans; + } +} diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1 @@ +/// diff --git a/src/lib/aes.ts b/src/lib/aes.ts new file mode 100644 index 000000000..36ff16b5b --- /dev/null +++ b/src/lib/aes.ts @@ -0,0 +1 @@ +export { gcm } from '@noble/ciphers/aes.js'; diff --git a/src/lib/epub.ts b/src/lib/epub.ts new file mode 100644 index 000000000..2cd231574 --- /dev/null +++ b/src/lib/epub.ts @@ -0,0 +1,285 @@ +import JSZip from 'jszip'; +// import { Plugin } from '@/types/plugin'; // apparently unused + +export type EpubOptions = { + title: string; + author?: string; + description?: string; + cover?: string; + language?: string; +}; + +export type ChapterData = { + title: string; + content: string; + path: string; +}; + +function sanitizeHtml(html: string): string { + // Remove script tags and their content + html = html.replace( + /)<[^<]*)*<\/script>/gi, + '', + ); + + // Remove style tags but keep inline styles + html = html.replace(/)<[^<]*)*<\/style>/gi, ''); + + // Ensure images have proper attributes + html = html.replace(/]*)>/gi, (match, attrs) => { + if (!attrs.includes('alt=')) { + return ``; + } + return match; + }); + + return html; +} + +function generateUUID(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +async function fetchCoverImage(coverUrl?: string): Promise<{ + data: ArrayBuffer | null; + mimeType: string; + extension: string; +}> { + if (!coverUrl) { + return { data: null, mimeType: '', extension: '' }; + } + + try { + let url = coverUrl; + if (!coverUrl.startsWith('http://') && !coverUrl.startsWith('https://')) { + url = coverUrl.startsWith('/') ? coverUrl : '/' + coverUrl; + } + + const response = await fetch(url); + + if (!response.ok) { + return { data: null, mimeType: '', extension: '' }; + } + + const arrayBuffer = await response.arrayBuffer(); + const contentType = response.headers.get('content-type') || 'image/jpeg'; + + let extension = 'jpg'; + if (contentType.includes('png')) extension = 'png'; + else if (contentType.includes('gif')) extension = 'gif'; + else if (contentType.includes('webp')) extension = 'webp'; + + if (extension === 'jpg') { + const urlLower = coverUrl.toLowerCase(); + if (urlLower.includes('.png')) extension = 'png'; + else if (urlLower.includes('.gif')) extension = 'gif'; + else if (urlLower.includes('.webp')) extension = 'webp'; + } + + return { + data: arrayBuffer, + mimeType: contentType, + extension, + }; + } catch (error) { + console.error('Error fetching cover image:', error); + return { data: null, mimeType: '', extension: '' }; + } +} + +export async function createEpub( + chapters: ChapterData[], + options: EpubOptions, +): Promise { + const zip = new JSZip(); + const uuid = generateUUID(); + const now = new Date().toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'; + + const coverImage = await fetchCoverImage(options.cover); + const hasCover = coverImage.data !== null; + + zip.file('mimetype', 'application/epub+zip'); + + zip.file( + 'META-INF/container.xml', + ` + + + + +`, + ); + + if (hasCover && coverImage.data) { + zip.file(`OEBPS/cover.${coverImage.extension}`, coverImage.data); + } + + const manifestItems = [ + '', + '', + ]; + + if (hasCover) { + manifestItems.push( + ``, + '', + ); + } + + manifestItems.push( + ...chapters.map( + (_, i) => + ``, + ), + ); + + const spineItems = hasCover + ? ['', ''] + : ['']; + + spineItems.push( + ...chapters.map((_, i) => ``), + ); + + const contentOpf = ` + + + urn:uuid:${uuid} + ${escapeXml(options.title)} + ${options.author ? `${escapeXml(options.author)}` : ''} + ${options.description ? `${escapeXml(options.description)}` : ''} + ${options.language || 'en'} + ${now} + ${now} + ${hasCover ? '' : ''} + + + ${manifestItems.join('\n ')} + + + ${spineItems.join('\n ')} + +`; + + zip.file('OEBPS/content.opf', contentOpf); + + const tocNcx = ` + + + + + + + + + ${escapeXml(options.title)} + + + ${chapters + .map( + (chapter, i) => ` + + + ${escapeXml(chapter.title)} + + + `, + ) + .join('')} + +`; + + zip.file('OEBPS/toc.ncx', tocNcx); + + const navXhtml = ` + + + + Navigation + + + + + +`; + + zip.file('OEBPS/nav.xhtml', navXhtml); + + if (hasCover) { + const coverXhtml = ` + + + + Cover + + + + + Cover + +`; + zip.file('OEBPS/cover.xhtml', coverXhtml); + } + + chapters.forEach((chapter, i) => { + const sanitizedContent = sanitizeHtml(chapter.content); + const chapterXhtml = ` + + + + ${escapeXml(chapter.title)} + + + + +

${escapeXml(chapter.title)}

+ ${sanitizedContent} + +`; + + zip.file(`OEBPS/chapter${i + 1}.xhtml`, chapterXhtml); + }); + + const blob = await zip.generateAsync({ + type: 'blob', + mimeType: 'application/epub+zip', + }); + return blob; +} + +function escapeXml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +export function downloadBlob(blob: Blob, filename: string): void { + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); +} diff --git a/src/lib/fetch.ts b/src/lib/fetch.ts new file mode 100644 index 000000000..40b78cb83 --- /dev/null +++ b/src/lib/fetch.ts @@ -0,0 +1,158 @@ +/* global Buffer, RequestInit */ + +import { parse as parseProto } from 'protobufjs'; + +export type FetchInit = { + headers?: Record | Headers; + method?: string; + body?: FormData | string; + [x: string]: + | string + | Record + | undefined + | FormData + | Headers; +}; + +const makeInit = async (init?: FetchInit) => { + const defaultHeaders: Record = { + 'Connection': 'keep-alive', + 'Accept': '*/*', + 'Accept-Language': '*', + 'Sec-Fetch-Mode': 'cors', + 'Accept-Encoding': 'gzip, deflate', + }; + if (init?.headers) { + if (init.headers instanceof Headers) { + for (const [name, value] of Object.entries(defaultHeaders)) { + if (!init.headers.get(name)) init.headers.set(name, value); + } + } else { + init.headers = { + ...defaultHeaders, + ...init.headers, + }; + } + } else { + init = { + ...init, + headers: defaultHeaders, + }; + } + return init; +}; + +/** + * Fetch with (Android) User Agent + * @param url + * @param init + * @returns response as normal fetch + */ +export async function fetchApi(url: string, init?: FetchInit) { + init = await makeInit(init); + console.log(url, init); + return await fetch(url, init as RequestInit); +} + +/** + * + * @param url + * @param init + * @returns base64 string of file + * @example fetchFile('https://avatars.githubusercontent.com/u/81222734?s=48&v=4'); + */ +export const fetchFile = async function (url: string, init?: FetchInit) { + init = await makeInit(init); + console.log(url, init); + try { + const res = await fetch(url, init as RequestInit); + if (!res.ok) return ''; + const arrayBuffer = await res.arrayBuffer(); + return Buffer.from(arrayBuffer).toString('base64'); + } catch (e) { + return ''; + } +}; + +/** + * + * @param url + * @param init + * @param encoding default: `utf-8`. link: https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder/encoding + * @returns plain text + * @example fetchText('https://github.com/LNReader/lnreader', {}, 'gbk'); + */ +export const fetchText = async function ( + url: string, + init?: FetchInit, + encoding?: string, +): Promise { + init = await makeInit(init); + console.log(url, init); + try { + const res = await fetch(url, init as RequestInit); + if (!res.ok) return ''; + const arrayBuffer = await res.arrayBuffer(); + const decoder = new TextDecoder(encoding); + return decoder.decode(arrayBuffer); + } catch (e) { + return ''; + } +}; + +type ProtoRequestInit = { + // merged .proto file + proto: string; + requestType: string; + requestData?: Record; + responseType: string; +}; + +const BYTE_MARK = BigInt((1 << 8) - 1); + +export const fetchProto = async function ( + protoInit: ProtoRequestInit, + url: string, + init?: FetchInit, +) { + const protoRoot = parseProto(protoInit.proto).root; + const RequestMessge = protoRoot.lookupType(protoInit.requestType); + if (RequestMessge.verify(protoInit.requestData || {})) { + throw new Error('Invalid Proto'); + } + // encode request data + const encodedrequest = RequestMessge.encode( + protoInit.requestData || {}, + ).finish(); + const requestLength = BigInt(encodedrequest.length); + const headers = new Uint8Array( + Array(5) + .fill(0) + .map((v, idx) => { + if (idx === 0) return 0; + return Number((requestLength >> BigInt(8 * (5 - idx - 1))) & BYTE_MARK); + }), + ); + init = await makeInit(init); + const bodyArray = new Uint8Array(headers.length + encodedrequest.length); + bodyArray.set(headers, 0); + bodyArray.set(encodedrequest, headers.length); + return fetch(url, { + method: 'POST', + ...init, + body: bodyArray, + } as RequestInit) + .then(r => r.arrayBuffer()) + .then(arr => { + // decode response data + const payload = new Uint8Array(arr); + const length = Number( + BigInt(payload[1] << 24) | + BigInt(payload[2] << 16) | + BigInt(payload[3] << 8) | + BigInt(payload[4]), + ); + const ResponseMessage = protoRoot.lookupType(protoInit.responseType); + return ResponseMessage.decode(payload.slice(5, 5 + length)); + }) as ReturnType; +}; diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 000000000..a0a2e0cae --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,110 @@ +export type StorageItem = { + created: Date; + value: T; + expires?: number; +}; + +class Storage { + private db: Record; + + /** + * Initializes a new instance of the Storage class. + */ + constructor() { + this.db = {}; + } + + /** + * Sets a key-value pair in storage. + * + * @param {string} key - The key to set. + * @param {T} value - The value to set. + * @param {Date | number} [expires] - Optional expiry date or time in milliseconds. + */ + set(key: string, value: T, expires?: Date | number): void { + this.db[key] = { + created: new Date(), + value, + expires: expires instanceof Date ? expires.getTime() : expires, + }; + } + + /** + * Retrieves the value for a given key from storage. + * + * @param {string} key - The key to retrieve the value for. + * @param {boolean} [raw] - Optional flag to return the raw stored item. + * @returns The stored value or undefined if key is not found. + */ + get(key: string, raw: true): StorageItem | undefined; + get(key: string, raw?: false): T | undefined; + get(key: string, raw?: boolean): T | StorageItem | undefined { + const item = this.db[key] as StorageItem | undefined; + if (item?.expires && Date.now() > item.expires) { + this.delete(key); + return undefined; + } + return raw ? item : item?.value; + } + + /** + * Retrieves all keys set by the `set` method. + * + * @returns {string[]} An array of keys. + */ + getAllKeys(): string[] { + return Object.keys(this.db); + } + + /** + * Deletes a key from the storage. + * + * @param key - The key to delete. + */ + delete(key: string): void { + delete this.db[key]; + } + + /** + * Clears all stored items from storage. + */ + clearAll(): void { + this.db = {}; + } +} + +// Export a singleton instance of the Storage class +export const storage = new Storage(); + +/* +These parameters cannot be implemented in `test-web`. +They are generated in the browser when js-scripts are executed +Read more + +https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage +https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage +*/ + +/** + * Represents the structure of a storage object with string keys and values. + */ +type StorageObject = Record; + +/** + * Represents a simplified version of the browser's localStorage. + */ +class LocalStorage { + private db: StorageObject; + + constructor() { + this.db = {}; + } + + get(): StorageObject | undefined { + return this.db; + } +} + +// Export singleton instances of LocalStorage and sessionStorage +export const localStorage = new LocalStorage(); +export const sessionStorage = new LocalStorage(); diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 000000000..30f5dbd5c --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,36 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +/** + * Merges Tailwind CSS classes with clsx + */ +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +/** + * Checks if a URL is absolute + */ +export const isUrlAbsolute = (url: string) => { + if (url) { + if (url.indexOf('//') === 0) { + return true; + } // URL is protocol-relative (= absolute) + if (url.indexOf('://') === -1) { + return false; + } // URL has no protocol (= relative) + if (url.indexOf('.') === -1) { + return false; + } // URL does not contain a dot, i.e. no TLD (= relative, possibly REST) + if (url.indexOf('/') === -1) { + return false; + } // URL does not contain a single slash (= relative) + if (url.indexOf(':') > url.indexOf('/')) { + return false; + } // The first colon comes after the first slash (= relative) + if (url.indexOf('://') < url.indexOf('.')) { + return true; + } // Protocol is defined before first dot (= absolute) + } + return false; // Anything else must be relative +}; diff --git a/src/libs/aes.ts b/src/libs/aes.ts new file mode 100644 index 000000000..48b58024c --- /dev/null +++ b/src/libs/aes.ts @@ -0,0 +1,5 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { gcm } from '../lib/aes'; diff --git a/src/libs/defaultCover.ts b/src/libs/defaultCover.ts new file mode 100644 index 000000000..f02172eb7 --- /dev/null +++ b/src/libs/defaultCover.ts @@ -0,0 +1,5 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { defaultCover } from '../types/constants'; diff --git a/src/libs/fetch.ts b/src/libs/fetch.ts new file mode 100644 index 000000000..be73680ab --- /dev/null +++ b/src/libs/fetch.ts @@ -0,0 +1,11 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { + fetchApi, + fetchText, + fetchProto, + fetchFile, + type FetchInit, +} from '../lib/fetch'; diff --git a/src/libs/filterInputs.ts b/src/libs/filterInputs.ts new file mode 100644 index 000000000..d934b0b98 --- /dev/null +++ b/src/libs/filterInputs.ts @@ -0,0 +1,13 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { FilterTypes } from '../types/filters'; +export type { + Filters, + Filter, + FilterOption, + FilterToValues, + FilterValueWithType, + AnyFilterValue, +} from '../types/filters'; diff --git a/src/libs/isAbsoluteUrl.ts b/src/libs/isAbsoluteUrl.ts new file mode 100644 index 000000000..2c9708304 --- /dev/null +++ b/src/libs/isAbsoluteUrl.ts @@ -0,0 +1,5 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { isUrlAbsolute } from '../lib/utils'; diff --git a/src/libs/novelStatus.ts b/src/libs/novelStatus.ts new file mode 100644 index 000000000..688bb382a --- /dev/null +++ b/src/libs/novelStatus.ts @@ -0,0 +1,5 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { NovelStatus } from '../types/constants'; diff --git a/src/libs/storage.ts b/src/libs/storage.ts new file mode 100644 index 000000000..988d3e026 --- /dev/null +++ b/src/libs/storage.ts @@ -0,0 +1,5 @@ +/** + * Backward compatibility for 3.0.0 - Re-exports from new location + * TODO: Remove in 4.0.0 + */ +export { storage, localStorage, sessionStorage } from '../lib/storage'; diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 000000000..97a1db24c --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,33 @@ +import 'cheerio'; +import 'htmlparser2'; +import 'dayjs'; +import 'protobufjs'; +import './index.css'; + +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App'; + +const { fetch: originalFetch } = window; + +window.fetch = async (...args) => { + const [resource, config] = args; + if (resource.toString().includes('localhost')) + return await originalFetch(resource, config); + const _res = await originalFetch('http://localhost:3000/' + resource, { + ...config, + credentials: 'include', + mode: 'cors', + }); + Object.defineProperty(_res, 'url', { + value: _res.url.includes('localhost') ? resource.toString() : _res.url, + }); + return _res; +}; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + , +); diff --git a/src/pages/home.tsx b/src/pages/home.tsx new file mode 100644 index 000000000..7bdf690e5 --- /dev/null +++ b/src/pages/home.tsx @@ -0,0 +1,194 @@ +import React, { useMemo, useState, useCallback } from 'react'; + +import { BookOpen, Search, Settings, Zap } from 'lucide-react'; +import PluginHeader from '../components/plugin-header'; + +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { Input } from '@/components/ui/input'; + +import plugins from '@plugins/index'; +import { useAppStore } from '@/store'; +import PopularNovelsSection from '@/components/popular-novels'; +import SearchNovelsSection from '@/components/search-novels'; +import ParseNovelSection from '@/components/parse-novel'; +import SettingsSection from '@/components/settings'; +import ParseChapterSection from '@/components/parse-chapter'; + +function PluginSidebar() { + const { plugin, selectPlugin } = useAppStore(state => state); + const [searchQuery, setSearchQuery] = useState(''); + + const filteredPlugins = useMemo( + () => + plugins.filter(p => + p.name.toLowerCase().includes(searchQuery.toLowerCase()), + ), + [searchQuery], + ); + + return ( + + ); +} + +function Home() { + const { plugin } = useAppStore(state => state); + + const [activeTab, setActiveTab] = useState('popular'); + + const handleNavigateToParseNovel = useCallback(() => { + setActiveTab('parse-novel'); + }, []); + + const handleNavigateToParseChapter = useCallback(() => { + setActiveTab('parse-chapter'); + }, []); + + return ( +
+ +
+ + + {/* Main Content */} +
+
+
+

+ Plugin Playground +

+

+ Explore and test {plugin?.name || 'plugin'} features +

+
+ + {/* Tabs */} + + + + + Popular + + + + Search + + + + Parse Novel + + + + Parse Chapter + + + + Settings + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +export default Home; diff --git a/src/provider/plugins.ts b/src/provider/plugins.ts new file mode 100644 index 000000000..fd1822d83 --- /dev/null +++ b/src/provider/plugins.ts @@ -0,0 +1,11 @@ +import plugins from '@plugins/index'; + +export const searchPlugins = (keyword: string) => { + return plugins.filter( + f => + f.name.toLowerCase().includes(keyword.toLowerCase()) || + f.id.toLowerCase().includes(keyword.toLowerCase()), + ); +}; + +export const getPlugin = (id: string) => plugins.find(f => f.id === id); diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 000000000..4b9d9594b --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,35 @@ +import { create, StateCreator } from 'zustand'; +import { PluginStore } from './pluginStore'; +import { NavigationStore } from './navigationStore'; + +export type AppStore = PluginStore & + NavigationStore & { + theme: 'light' | 'dark'; + setTheme(value: 'light' | 'dark'): void; + }; + +// Helper types to use "slicing" like in Redux... We could just not use slicing, but eh +export type SetStore = Parameters>[0]; +export type GetStore = Parameters>[1]; +export type StoreCreator = (s: SetStore, g: GetStore) => T; + +const getInitialTheme = (): 'light' | 'dark' => { + const stored = localStorage.getItem('theme'); + if (stored === 'light' || stored === 'dark') { + return stored; + } + return 'light'; +}; + +export const useAppStore = create((set: SetStore, get: GetStore) => ({ + ...PluginStore(set, get), + ...NavigationStore(set, get), + theme: getInitialTheme(), + setTheme(theme: 'light' | 'dark') { + set(state => ({ + ...state, + theme, + })); + localStorage.setItem('theme', theme); + }, +})); diff --git a/src/store/navigationStore.ts b/src/store/navigationStore.ts new file mode 100644 index 000000000..460da11c8 --- /dev/null +++ b/src/store/navigationStore.ts @@ -0,0 +1,55 @@ +import { StoreCreator } from '.'; + +export type NavigationStore = { + parseNovelPath?: string; + parseChapterPath?: string; + shouldAutoSubmitNovel: boolean; + shouldAutoSubmitChapter: boolean; + setParseNovelPath(path: string, autoSubmit?: boolean): void; + clearParseNovelPath(): void; + setParseChapterPath(path: string, autoSubmit?: boolean): void; + clearParseChapterPath(): void; +}; + +/** + * @param set State setter for use inside actions + * @param get State getter for use inside actions, outside of State setter + */ +export const NavigationStore: StoreCreator = set => ({ + parseNovelPath: undefined, + parseChapterPath: undefined, + shouldAutoSubmitNovel: false, + shouldAutoSubmitChapter: false, + + setParseNovelPath(path: string, autoSubmit = true) { + set(state => ({ + ...state, + parseNovelPath: path, + shouldAutoSubmitNovel: autoSubmit, + })); + }, + + clearParseNovelPath() { + set(state => ({ + ...state, + parseNovelPath: undefined, + shouldAutoSubmitNovel: false, + })); + }, + + setParseChapterPath(path: string, autoSubmit = true) { + set(state => ({ + ...state, + parseChapterPath: path, + shouldAutoSubmitChapter: autoSubmit, + })); + }, + + clearParseChapterPath() { + set(state => ({ + ...state, + parseChapterPath: undefined, + shouldAutoSubmitChapter: false, + })); + }, +}); diff --git a/src/store/pluginStore.ts b/src/store/pluginStore.ts new file mode 100644 index 000000000..f6afb146f --- /dev/null +++ b/src/store/pluginStore.ts @@ -0,0 +1,47 @@ +import { Plugin } from '@/types/plugin'; +import { StoreCreator } from '.'; +import { getPlugin } from '@/provider/plugins'; +import plugins from '@plugins/index'; + +export type PluginStore = { + pluginItem?: Plugin.PluginItem; + plugin?: Plugin.PluginBase; + selectPlugin(plugin: Plugin.PluginItem, updateURL?: boolean): void; +}; + +const loadPluginFromURL = () => { + const urlParams = new URLSearchParams(window.location.search); + const pluginId = urlParams.get('plugin'); + if (pluginId) { + const pluginItem = plugins.find(p => p.id === pluginId); + if (pluginItem) { + return { + pluginItem, + plugin: getPlugin(pluginItem.id), + }; + } + } + return {}; +}; + +/** + * @param set State setter for use inside actions + * @param get State getter for use inside actions, outside of State setter + */ +export const PluginStore: StoreCreator = set => ({ + ...loadPluginFromURL(), + + selectPlugin(pluginItem, updateURL = true) { + set(state => ({ + ...state, + pluginItem, + plugin: getPlugin(pluginItem.id), + })); + + if (updateURL) { + const url = new URL(window.location.href); + url.searchParams.set('plugin', pluginItem.id); + window.history.pushState({}, '', url); + } + }, +}); diff --git a/src/types/constants.ts b/src/types/constants.ts new file mode 100644 index 000000000..b81c50cce --- /dev/null +++ b/src/types/constants.ts @@ -0,0 +1,12 @@ +export const NovelStatus = { + Unknown: 'Unknown', + Ongoing: 'Ongoing', + Completed: 'Completed', + Licensed: 'Licensed', + PublishingFinished: 'Publishing Finished', + Cancelled: 'Cancelled', + OnHiatus: 'On Hiatus', +} as const; + +export const defaultCover = + 'https://github.com/LNReader/lnreader-plugins/blob/main/icons/src/coverNotAvailable.jpg?raw=true'; diff --git a/src/types/filters.ts b/src/types/filters.ts new file mode 100644 index 000000000..8cc7eeb95 --- /dev/null +++ b/src/types/filters.ts @@ -0,0 +1,130 @@ +/** + * Filter's Options user can choose from + */ +export type FilterOption = { + readonly label: string; + readonly value: string; +}; + +/** + * Every currently implemented FilterType + */ +export enum FilterTypes { + TextInput = 'Text', + Picker = 'Picker', + CheckboxGroup = 'Checkbox', + Switch = 'Switch', + ExcludableCheckboxGroup = 'XCheckbox', +} + +type SwitchFilter = { + type: FilterTypes.Switch; + /** Default value */ + value: boolean; +}; + +type TextFilter = { + type: FilterTypes.TextInput; + /** Default value */ + value: string; +}; + +type CheckboxFilter = { + type: FilterTypes.CheckboxGroup; + options: readonly FilterOption[]; + /** Default value */ + value: string[]; +}; +type PickerFilter = { + type: FilterTypes.Picker; + options: readonly FilterOption[]; + /** Default value */ + value: string; +}; + +type ExcludableCheckboxFilter = { + type: FilterTypes.ExcludableCheckboxGroup; + options: readonly FilterOption[]; + /** Default value */ + value: { + /** Checkboxes marked as included */ + include?: string[]; + /** Checkboxes marked as excluded */ + exclude?: string[]; + }; +}; + +/** + * key - filter pairs + */ +export type Filters = Record>; + +/** Mapping of each FilterType to a Filter */ +type FilterFromType = { + [FilterTypes.CheckboxGroup]: CheckboxFilter; + [FilterTypes.ExcludableCheckboxGroup]: ExcludableCheckboxFilter; + [FilterTypes.Picker]: PickerFilter; + [FilterTypes.Switch]: SwitchFilter; + [FilterTypes.TextInput]: TextFilter; +}; + +/** + * Get type of a single filter type from the {@link FilterType} + */ +export type Filter = { + label: string; +} & FilterFromType[Type]; + +/** + * Strip {@link FilterObject} object from 'label' and 'options' to get key - filter_value pairs + * @see {@link ValueOfFilter} + */ +export type FilterToValues< + FilterObject extends Record | undefined, +> = FilterObject extends undefined + ? undefined + : { + // copy the Filters object, but just get {value,type} pairs instead of the whole Filter object + [SingleFilter in keyof FilterObject]: FilterValueWithType< + FilterType[SingleFilter]> + >; + }; + +/** + * Get value type for a {@link Filter} given it's FilterType + * @see {@link FilterTypes} + */ +export type ValueOfFilter = + T extends FilterTypes.CheckboxGroup + ? CheckboxFilter['value'] + : T extends FilterTypes.Picker + ? PickerFilter['value'] + : T extends FilterTypes.Switch + ? SwitchFilter['value'] + : T extends FilterTypes.TextInput + ? TextFilter['value'] + : T extends FilterTypes.ExcludableCheckboxGroup + ? ExcludableCheckboxFilter['value'] + : never; + +/** Get {@link Filter}'s type */ +export type FilterType = T extends { + type: infer K; +} + ? K extends FilterTypes + ? K + : never + : never; + +/** Get {type, value} types for given FilterType + * @see {@link ValueOfFilter} + */ +export type FilterValueWithType = { + type: T; + value: ValueOfFilter; +}; + +/** + * Any possible filter value + */ +export type AnyFilterValue = ValueOfFilter; diff --git a/src/types/plugin.ts b/src/types/plugin.ts new file mode 100644 index 000000000..477a01734 --- /dev/null +++ b/src/types/plugin.ts @@ -0,0 +1,119 @@ +import { FilterToValues, Filters } from '@libs/filterInputs'; +export namespace Plugin { + export type ChapterItem = { + name: string; + path: string; + /** + * "YYYY-MM-DD" format or ISO string format + * ```js + * chapter.releaseTime = '2023-12-02'; + * chapter.releaseTime = new Date(2023, 12, 02).toISOString(); + * ``` + * or just a string + */ + releaseTime?: string | null; + chapterNumber?: number; + /** + * For novel without pages only + */ + page?: string; + }; + export type NovelItem = { + name: string; + path: string; + cover?: string; + }; + export type SourceNovel = { + /** Comma separated genre list -> "action,fantasy,romance" */ + genres?: string; + summary?: string; + author?: string; + artist?: string; + status?: string; + /** Rating out of 5 as float */ + rating?: number; + chapters?: ChapterItem[]; + } & NovelItem; + + export type SourcePage = { + chapters: ChapterItem[]; + }; + + export type PopularNovelsOptions< + Q extends Filters | undefined = Filters | undefined, + > = { + showLatestNovels?: boolean; + filters: Q extends undefined ? undefined : FilterToValues; + }; + export type PluginItem = { + id: string; + name: string; + version: string; + icon: string; + site: string; + }; + export type ImageRequestInit = { + method?: string; + headers?: Record; + body?: string; + }; + + export type PluginBase = { + id: string; + name: string; + /** + * Relative path without static. E.g: + * ```js + * "src/vi/hakolightnovel/icon.png" + * ``` + */ + icon: string; + customJS?: string; + customCSS?: string; + site: string; + imageRequestInit?: ImageRequestInit; + filters?: Filters; + version: string; + //flag indicates whether access to LocalStorage, SesesionStorage is required. + webStorageUtilized?: boolean; + popularNovels( + pageNo: number, + options: PopularNovelsOptions, + ): Promise; + /** + * + * @param novelPath + * @returns novel metadata and its first page + */ + parseNovel(novelPath: string): Promise; + parseChapter(chapterPath: string): Promise; + searchNovels(searchTerm: string, pageNo: number): Promise; + resolveUrl?(path: string, isNovel?: boolean): string; + }; + + export type PagePlugin = { + parseNovel( + novelPath: string, + ): Promise; + parsePage(novelPath: string, page: string): Promise; + } & PluginBase; +} + +export namespace HTMLParser2Util { + type HandlerBase = { + onopentag?(name: string, attribs: Record): void; + ontext?(data: string): void; + onclosetag?(name: string, isImplied: boolean): void; + }; + + export type Handler = { + isStarted?: boolean; + isDone?: boolean; + } & HandlerBase; + + // route htmlparser2 event to handlers + export type HandlerRouter = { + handlers: Record; + action: ActionType; + } & HandlerBase; +} diff --git a/src/types/types.ts b/src/types/types.ts new file mode 100644 index 000000000..2fc7ca7e8 --- /dev/null +++ b/src/types/types.ts @@ -0,0 +1,18 @@ +import { Plugin } from '@/types/plugin'; + +export type PluginList = Record; + +export enum FetchMode { + PROXY, + NODE_FETCH, + CURL, +} + +export type ServerSetting = { + CLIENT_HOST: string; + fetchMode: FetchMode; + cookies?: string; + disAllowedRequestHeaders: string[]; + disAllowResponseHeaders: string[]; + useUserAgent: boolean; +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..7d06e7b10 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,132 @@ +{ + "compilerOptions": { + "jsx": "react", + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [ + // "es2016", + // "dom", + // "es5" + // ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ES2020" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": "." /* Specify the base directory to resolve non-relative module names. */, + "paths": { + "@/*": ["./src/*"], + "@plugins/*": ["./plugins/*"], + "@libs/*": ["./src/libs/*"], + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./.js/" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + "allowJs": true, + "checkJs": false, + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + }, + "exclude": [ + "./plugins/**/*.broken.ts", + "./.js/**/*", + "scripts/**", + "docs/**", + "public/**", + ".github/**", + ".husky/**", + "node_modules/**", + "./plugins/multisrc/*", + ], +} diff --git a/tsconfig.production.json b/tsconfig.production.json new file mode 100644 index 000000000..17a7a6252 --- /dev/null +++ b/tsconfig.production.json @@ -0,0 +1,128 @@ +{ + "compilerOptions": { + "jsx": "react", + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES5" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [ + // "es2016", + // "dom", + // "es5" + // ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */, + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "CommonJS" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node10" /* Specify how TypeScript looks up a file from a given module specifier. */, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@plugins/*": ["./plugins/*"], + "@libs/*": ["./src/libs/*"] + } /* Specify a set of entries that re-map imports to additional lookup locations. */, + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true /* Enable importing .json files. */, + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true /* Enable error reporting in type-checked JavaScript files. */, + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./.js/plugins" /* Specify an output folder for all emitted files. */, + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + "allowJs": true, + "checkJs": false, + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "noCheck": true, + "noResolve": true + }, + "exclude": [ + "./plugins/**/*.broken.ts", + "./.js/**/*", + "scripts/**", + "docs/**", + "./src/*.*", + "./*.*", + "public/**", + ".github/**", + ".husky/**", + "node_modules/**", + "./src/**", + "./plugins/multisrc/*" + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..be796289f --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import { nodePolyfills } from 'vite-plugin-node-polyfills'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import tailwindcss from '@tailwindcss/vite'; +import { proxyHandlerMiddle, proxySettingMiddleware } from './proxy'; + +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + tailwindcss(), + nodePolyfills(), + react({ devTarget: 'es5' }), + { + name: 'proxy', + configureServer: server => { + server.middlewares.use('/settings', proxySettingMiddleware); + server.middlewares.use('/https:', proxyHandlerMiddle); + }, + }, + ], + resolve: { + alias: { + '@': path.resolve(dirname, './src'), + '@plugins': path.resolve(dirname, './plugins'), + '@libs': path.resolve(dirname, './src/libs'), + }, + }, + server: { + port: 3000, + open: true, + }, +});