refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc * feat: Overhaul Playground UI * feat: Add Filters to New UI * feat: Persist Selected Plugin * feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts [skip ci] * feat: UX Improvements * chore: Update Plugin Path in Publish Workflow * fix: Add Backward Compatibility * chore: Update Plugin Publishing Script
This commit is contained in:
committed by
GitHub
parent
252beff0bf
commit
20c2b75b01
@@ -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)
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
{
|
||||
"filters": {
|
||||
"type": {
|
||||
"type": "Picker",
|
||||
"label": "Novel Listing",
|
||||
"value": "sort/nov-love-popular",
|
||||
"options": [
|
||||
{
|
||||
"label": "Hot Novel",
|
||||
"value": "sort/nov-love-hot"
|
||||
},
|
||||
{
|
||||
"label": "Completed Novel",
|
||||
"value": "sort/nov-love-complete"
|
||||
},
|
||||
{
|
||||
"label": "Most Popular",
|
||||
"value": "sort/nov-love-popular"
|
||||
}
|
||||
]
|
||||
},
|
||||
"genres": {
|
||||
"type": "Picker",
|
||||
"label": "Genre",
|
||||
"value": "",
|
||||
"options": [
|
||||
{
|
||||
"label": "Action",
|
||||
"value": "nov-love-genres/action"
|
||||
},
|
||||
{
|
||||
"label": "Adventure",
|
||||
"value": "nov-love-genres/adventure"
|
||||
},
|
||||
{
|
||||
"label": "Anime & comics",
|
||||
"value": "nov-love-genres/anime-&-comics"
|
||||
},
|
||||
{
|
||||
"label": "Comedy",
|
||||
"value": "nov-love-genres/comedy"
|
||||
},
|
||||
{
|
||||
"label": "Drama",
|
||||
"value": "nov-love-genres/drama"
|
||||
},
|
||||
{
|
||||
"label": "Eastern",
|
||||
"value": "nov-love-genres/eastern"
|
||||
},
|
||||
{
|
||||
"label": "Fan-fiction",
|
||||
"value": "nov-love-genres/fan-fiction"
|
||||
},
|
||||
{
|
||||
"label": "Fanfiction",
|
||||
"value": "nov-love-genres/fanfiction"
|
||||
},
|
||||
{
|
||||
"label": "Fantasy",
|
||||
"value": "nov-love-genres/fantasy"
|
||||
},
|
||||
{
|
||||
"label": "Game",
|
||||
"value": "nov-love-genres/game"
|
||||
},
|
||||
{
|
||||
"label": "Games",
|
||||
"value": "nov-love-genres/games"
|
||||
},
|
||||
{
|
||||
"label": "Gender bender",
|
||||
"value": "nov-love-genres/gender-bender"
|
||||
},
|
||||
{
|
||||
"label": "General",
|
||||
"value": "nov-love-genres/general"
|
||||
},
|
||||
{
|
||||
"label": "Harem",
|
||||
"value": "nov-love-genres/harem"
|
||||
},
|
||||
{
|
||||
"label": "Historical",
|
||||
"value": "nov-love-genres/historical"
|
||||
},
|
||||
{
|
||||
"label": "Horror",
|
||||
"value": "nov-love-genres/horror"
|
||||
},
|
||||
{
|
||||
"label": "Isekai",
|
||||
"value": "nov-love-genres/isekai"
|
||||
},
|
||||
{
|
||||
"label": "Josei",
|
||||
"value": "nov-love-genres/josei"
|
||||
},
|
||||
{
|
||||
"label": "Litrpg",
|
||||
"value": "nov-love-genres/litrpg"
|
||||
},
|
||||
{
|
||||
"label": "Magic",
|
||||
"value": "nov-love-genres/magic"
|
||||
},
|
||||
{
|
||||
"label": "Magical realism",
|
||||
"value": "nov-love-genres/magical-realism"
|
||||
},
|
||||
{
|
||||
"label": "Martial arts",
|
||||
"value": "nov-love-genres/martial-arts"
|
||||
},
|
||||
{
|
||||
"label": "Mature",
|
||||
"value": "nov-love-genres/mature"
|
||||
},
|
||||
{
|
||||
"label": "Mecha",
|
||||
"value": "nov-love-genres/mecha"
|
||||
},
|
||||
{
|
||||
"label": "Modern life",
|
||||
"value": "nov-love-genres/modern-life"
|
||||
},
|
||||
{
|
||||
"label": "Myster",
|
||||
"value": "nov-love-genres/myster"
|
||||
},
|
||||
{
|
||||
"label": "Mystery",
|
||||
"value": "nov-love-genres/mystery"
|
||||
},
|
||||
{
|
||||
"label": "Other",
|
||||
"value": "nov-love-genres/other"
|
||||
},
|
||||
{
|
||||
"label": "Psychological",
|
||||
"value": "nov-love-genres/psychological"
|
||||
},
|
||||
{
|
||||
"label": "Reincarnation",
|
||||
"value": "nov-love-genres/reincarnation"
|
||||
},
|
||||
{
|
||||
"label": "Report story",
|
||||
"value": "nov-love-genres/report-story"
|
||||
},
|
||||
{
|
||||
"label": "Romance",
|
||||
"value": "nov-love-genres/romance"
|
||||
},
|
||||
{
|
||||
"label": "Romance.smut",
|
||||
"value": "nov-love-genres/romance.smut"
|
||||
},
|
||||
{
|
||||
"label": "School life",
|
||||
"value": "nov-love-genres/school-life"
|
||||
},
|
||||
{
|
||||
"label": "Sci-fi",
|
||||
"value": "nov-love-genres/sci-fi"
|
||||
},
|
||||
{
|
||||
"label": "Seinen",
|
||||
"value": "nov-love-genres/seinen"
|
||||
},
|
||||
{
|
||||
"label": "Shoujo",
|
||||
"value": "nov-love-genres/shoujo"
|
||||
},
|
||||
{
|
||||
"label": "Shoujo ai",
|
||||
"value": "nov-love-genres/shoujo-ai"
|
||||
},
|
||||
{
|
||||
"label": "Shounen",
|
||||
"value": "nov-love-genres/shounen"
|
||||
},
|
||||
{
|
||||
"label": "Shounen ai",
|
||||
"value": "nov-love-genres/shounen-ai"
|
||||
},
|
||||
{
|
||||
"label": "Slice of life",
|
||||
"value": "nov-love-genres/slice-of-life"
|
||||
},
|
||||
{
|
||||
"label": "Smut",
|
||||
"value": "nov-love-genres/smut"
|
||||
},
|
||||
{
|
||||
"label": "Sports",
|
||||
"value": "nov-love-genres/sports"
|
||||
},
|
||||
{
|
||||
"label": "Supernatural",
|
||||
"value": "nov-love-genres/supernatural"
|
||||
},
|
||||
{
|
||||
"label": "System",
|
||||
"value": "nov-love-genres/system"
|
||||
},
|
||||
{
|
||||
"label": "Thriller",
|
||||
"value": "nov-love-genres/thriller"
|
||||
},
|
||||
{
|
||||
"label": "Tragedy",
|
||||
"value": "nov-love-genres/tragedy"
|
||||
},
|
||||
{
|
||||
"label": "Urban",
|
||||
"value": "nov-love-genres/urban"
|
||||
},
|
||||
{
|
||||
"label": "Urban life",
|
||||
"value": "nov-love-genres/urban-life"
|
||||
},
|
||||
{
|
||||
"label": "Video games",
|
||||
"value": "nov-love-genres/video-games"
|
||||
},
|
||||
{
|
||||
"label": "War",
|
||||
"value": "nov-love-genres/war"
|
||||
},
|
||||
{
|
||||
"label": "Wuxia",
|
||||
"value": "nov-love-genres/wuxia"
|
||||
},
|
||||
{
|
||||
"label": "Xianxia",
|
||||
"value": "nov-love-genres/xianxia"
|
||||
},
|
||||
{
|
||||
"label": "Xuanhuan",
|
||||
"value": "nov-love-genres/xuanhuan"
|
||||
},
|
||||
{
|
||||
"label": "Yaoi",
|
||||
"value": "nov-love-genres/yaoi"
|
||||
},
|
||||
{
|
||||
"label": "Yuri",
|
||||
"value": "nov-love-genres/yuri"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
const plugin = new ReadNovelFullPlugin(${JSON.stringify(source)});
|
||||
export default plugin;
|
||||
`.trim();
|
||||
return {
|
||||
lang: source.options?.lang || 'English',
|
||||
filename: source.sourceName,
|
||||
pluginScript,
|
||||
};
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -0,0 +1,102 @@
|
||||
[
|
||||
{
|
||||
"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": "boxnovel",
|
||||
"sourceSite": "https://novlove.com/",
|
||||
"sourceName": "BoxNovel",
|
||||
"options": {
|
||||
"latestPage": "sort/nov-love-daily-update",
|
||||
"searchPage": "search"
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": "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
|
||||
}
|
||||
},
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,758 @@
|
||||
import { Parser } from 'htmlparser2';
|
||||
import { fetchApi } from '@libs/fetch';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { NovelStatus } from '@libs/novelStatus';
|
||||
import { Filters } from '@libs/filterInputs';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type ReadNovelFullMetadata = {
|
||||
id: string;
|
||||
sourceSite: string;
|
||||
sourceName: string;
|
||||
options: ReadNovelFullOptions;
|
||||
filters?: any;
|
||||
};
|
||||
|
||||
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.1.${2 + 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,
|
||||
): 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);
|
||||
const html = await response.text();
|
||||
|
||||
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();
|
||||
|
||||
type EscapeChar = '&' | '<' | '>' | '"' | "'" | ' ';
|
||||
const escapeRegex = /[&<>"' ]/g;
|
||||
const escapeMap: Record<EscapeChar, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
' ': ' ',
|
||||
};
|
||||
const escapeHtml = (text: string): string =>
|
||||
text.replace(escapeRegex, char => escapeMap[char as EscapeChar]);
|
||||
|
||||
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') {
|
||||
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) {
|
||||
chapterHtml.push(escapeHtml(text));
|
||||
}
|
||||
},
|
||||
|
||||
onclosetag(name) {
|
||||
const state = currentState();
|
||||
|
||||
if (state === ParsingState.Hidden) {
|
||||
if (name === 'sub') {
|
||||
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: RequestInit | 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,
|
||||
}
|
||||
Reference in New Issue
Block a user