diff --git a/plugins/multisrc/novelfire/filters/novelfire.json b/plugins/multisrc/novelfire/filters/novelfire.json new file mode 100644 index 000000000..f7648df6c --- /dev/null +++ b/plugins/multisrc/novelfire/filters/novelfire.json @@ -0,0 +1,454 @@ +{ + "filters": { + "language": { + "label": "Language", + "value": [], + "options": [ + { + "label": "Chinese Novel", + "value": "1" + }, + { + "label": "Japanese Novel", + "value": "3" + }, + { + "label": "English Novel", + "value": "4" + } + ], + "type": "Checkbox" + }, + "genre_operator": { + "label": "Genres (And/Or/Exclude)", + "value": "and", + "options": [ + { + "label": "AND", + "value": "and" + }, + { + "label": "OR", + "value": "or" + }, + { + "label": "EXCLUDE", + "value": "exclude" + } + ], + "type": "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": "Checkbox" + }, + "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": "Picker" + }, + "rating_operator": { + "label": "Rating (Min/Max)", + "value": "min", + + "options": [ + { + "label": "min", + "value": "min" + }, + { + "label": "max", + "value": "max" + } + ], + "type": "Picker" + }, + "rating": { + "label": "Rating", + "value": "0", + + "options": [ + { + "label": "none", + "value": "0" + }, + { + "label": "1", + "value": "1" + }, + { + "label": "2", + "value": "2" + }, + { + "label": "3", + "value": "3" + }, + { + "label": "4", + "value": "4" + }, + { + "label": "5", + "value": "5" + } + ], + "type": "Picker" + }, + "status": { + "label": "Translation Status", + "value": "-1", + + "options": [ + { + "label": "All", + "value": "-1" + }, + { + "label": "Completed", + "value": "1" + }, + { + "label": "Ongoing", + "value": "0" + } + ], + "type": "Picker" + }, + "sort": { + "label": "Sort Results By", + "value": "date", + + "options": [ + { + "label": "Last Updated (Newest)", + "value": "date" + }, + { + "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": "Chapter Count (Most)", + "value": "chapter-count-most" + }, + { + "label": "Title (A>Z)", + "value": "abc" + }, + { + "label": "Title (Z>A)", + "value": "cba" + } + ], + "type": "Picker" + }, + "tagcon": { + "label": "Tags (And/Or)", + "value": "and", + + "options": [ + { + "label": "AND", + "value": "and" + }, + { + "label": "OR", + "value": "or" + } + ], + "type": "Picker" + }, + "author": { + "label": "Author", + "value": "", + "type": "Text" + } + } +} diff --git a/plugins/multisrc/novelfire/filters/novelphoenix.json b/plugins/multisrc/novelfire/filters/novelphoenix.json new file mode 100644 index 000000000..91a484018 --- /dev/null +++ b/plugins/multisrc/novelfire/filters/novelphoenix.json @@ -0,0 +1,456 @@ +{ + "filters": { + "language": { + "label": "Language", + "value": [], + + "options": [ + { + "label": "Chinese Novel", + "value": "1" + }, + { + "label": "Japanese Novel", + "value": "3" + }, + { + "label": "English Novel", + "value": "4" + } + ], + "type": "Checkbox" + }, + "genre_operator": { + "label": "Genres (And/Or/Exclude)", + "value": "and", + "options": [ + { + "label": "AND", + "value": "and" + }, + { + "label": "OR", + "value": "or" + }, + { + "label": "EXCLUDE", + "value": "exclude" + } + ], + "type": "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": "Checkbox" + }, + "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": "Picker" + }, + "rating_operator": { + "label": "Rating (Min/Max)", + "value": "min", + + "options": [ + { + "label": "min", + "value": "min" + }, + { + "label": "max", + "value": "max" + } + ], + "type": "Picker" + }, + "rating": { + "label": "Rating", + "value": "0", + + "options": [ + { + "label": "none", + "value": "0" + }, + { + "label": "1", + "value": "1" + }, + { + "label": "2", + "value": "2" + }, + { + "label": "3", + "value": "3" + }, + { + "label": "4", + "value": "4" + }, + { + "label": "5", + "value": "5" + } + ], + "type": "Picker" + }, + "status": { + "label": "Translation Status", + "value": "-1", + + "options": [ + { + "label": "All", + "value": "-1" + }, + { + "label": "Completed", + "value": "1" + }, + { + "label": "Ongoing", + "value": "0" + } + ], + "type": "Picker" + }, + "sort": { + "label": "Sort Results By", + "value": "date", + + "options": [ + { + "label": "Last Updated (Newest)", + "value": "date" + }, + { + "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": "Chapter Count (Most)", + "value": "chapter-count-most" + }, + { + "label": "Title (A>Z)", + "value": "abc" + }, + { + "label": "Title (Z>A)", + "value": "cba" + } + ], + "type": "Picker" + }, + "tagcon": { + "label": "Tags (And/Or)", + "value": "and", + + "options": [ + { + "label": "AND", + "value": "and" + }, + { + "label": "OR", + "value": "or" + } + ], + "type": "Picker" + }, + "author": { + "label": "Author", + "value": "", + "type": "Text" + } + } +} diff --git a/plugins/multisrc/novelfire/generator.js b/plugins/multisrc/novelfire/generator.js new file mode 100644 index 000000000..387851414 --- /dev/null +++ b/plugins/multisrc/novelfire/generator.js @@ -0,0 +1,44 @@ +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'), + 'utf-8', + ); + // strip `// prettier-ignore` comments (JSONC) since JSON.parse doesn't allow them + source.filters = JSON.parse( + filters.replace(/^\s*\/\/[^\n]*$/gm, ''), + ).filters; + } + console.log( + `[novelfire] Generating: ${source.id}${' '.repeat(20 - source.id.length)} ${source.filters ? '🔎with filters🔍' : '🚫no filters🚫'}`, + ); + return generator(source); + }); +}; + +const generator = function generator(source) { + const NovelFireTemplate = readFileSync(join(folder, 'template.ts'), { + encoding: 'utf-8', + }); + + const pluginScript = ` +${NovelFireTemplate} +const plugin = new NovelFirePlugin(${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/novelfire/get_filters.js b/plugins/multisrc/novelfire/get_filters.js new file mode 100644 index 000000000..dee0b60da --- /dev/null +++ b/plugins/multisrc/novelfire/get_filters.js @@ -0,0 +1,290 @@ +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: { + language: { + label: 'Language', + value: [], + options: [], + type: 'Checkbox', + }, + genre_operator: { + label: 'Genres (And/Or/Exclude)', + value: 'and', + options: [], + type: 'Picker', + }, + genres: { + label: 'Genres', + value: [], + options: [], + type: 'Checkbox', + }, + chapters: { + label: 'Chapters', + value: '0', + options: [], + type: 'Picker', + }, + rating_operator: { + label: 'Rating (Min/Max)', + value: 'min', + options: [], + type: 'Picker', + }, + rating: { + label: 'Rating', + value: '0', + options: [], + type: 'Picker', + }, + status: { + label: 'Translation Status', + value: '-1', + options: [], + type: 'Picker', + }, + sort: { + label: 'Sort Results By', + value: 'rank-top', + options: [], + type: 'Picker', + }, + tagcon: { + label: 'Tags (And/Or)', + value: 'and', + options: [], + type: 'Picker', + }, + author: { + label: 'Author', + value: '', + type: 'Text', + }, + }, + }; + + // ==================== Language ==================== + filters.filters.language.label = + $('.search-adv-form > label').text().trim() || 'Language'; + $('input[name="country_id[]"]').each((i, el) => { + filters.filters.language.options.push({ + label: $(el).parent().text().trim() || $(el).next('label').text().trim(), + value: $(el).attr('value'), + }); + }); + + // ==================== Genre operator ==================== + $('select[name="ctgcon"] option').each((i, el) => { + filters.filters.genre_operator.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const genreOpSelected = + $('select[name="ctgcon"] option:selected').attr('value') || 'and'; + filters.filters.genre_operator.value = genreOpSelected; + + // ==================== Genres ==================== + $('input[name="categories[]"]').each((i, el) => { + filters.filters.genres.options.push({ + label: $(el).parent().text().trim() || $(el).next('label').text().trim(), + value: $(el).attr('value'), + }); + }); + + // ==================== Chapters ==================== + $('select[name="totalchapter"] option').each((i, el) => { + filters.filters.chapters.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const chaptersSelected = + $('select[name="totalchapter"] option:selected').attr('value') || '0'; + filters.filters.chapters.value = chaptersSelected; + + // ==================== Rating operator ==================== + $('select[name="ratcon"] option').each((i, el) => { + filters.filters.rating_operator.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const ratconSelected = + $('select[name="ratcon"] option:selected').attr('value') || 'min'; + filters.filters.rating_operator.value = ratconSelected; + + // ==================== Rating ==================== + $('select[name="rating"] option').each((i, el) => { + filters.filters.rating.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const ratingSelected = + $('select[name="rating"] option:selected').attr('value') || '0'; + filters.filters.rating.value = ratingSelected; + + // ==================== Status ==================== + $('select[name="status"] option').each((i, el) => { + filters.filters.status.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const statusSelected = + $('select[name="status"] option:selected').attr('value') || '-1'; + filters.filters.status.value = statusSelected; + + // ==================== Sort ==================== + $('select[name="sort"] option').each((i, el) => { + filters.filters.sort.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const sortSelected = + $('select[name="sort"] option:selected').attr('value') || 'rank-top'; + filters.filters.sort.value = sortSelected; + + // ==================== Tag operator ==================== + $('select[name="tagcon"] option').each((i, el) => { + filters.filters.tagcon.options.push({ + label: $(el).text().trim(), + value: $(el).attr('value'), + }); + }); + const tagconSelected = + $('select[name="tagcon"] option:selected').attr('value') || 'and'; + filters.filters.tagcon.value = tagconSelected; + + // ==================== Author ==================== + filters.filters.author.label = + $('input[name="author"]').prev('label').text().trim() || + $('input[name="author"]').parent().prev().text().trim() || + 'Author'; + + // ==================== checks ==================== + if ( + filters.filters.genres.options.length == 0 || + filters.filters.language.options.length == 0 || + filters.filters.sort.options.length == 0 + ) { + console.error( + `🚨Error in filters for ${name} please fix manually (${path.join(__dirname, 'filters', name + '.json')})🚨`, + ); + } + + // add `// prettier-ignore` above each `options` array so prettier does not + // reformat them (the generator strips these comments before parsing) + const json = JSON.stringify(filters, null, 2).replace( + /\n(\s*)"options": \[/g, + '\n$1// prettier-ignore\n$1"options": [', + ); + + fs.writeFileSync( + path.join(__dirname, 'filters', name + '.json'), + json + '\n', + ); + console.log(`✅Filters created successfully for ${name}✅`); +} + +async function getFiltersFromURL(name, url) { + const response = await fetch(url + '/search-adv'); + 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}/search-adv (at the end press ENTER then press CTRL+C) +(to make it faster you can run \`$(".search-adv-form").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/novelfire/sources.json b/plugins/multisrc/novelfire/sources.json new file mode 100644 index 000000000..616dad192 --- /dev/null +++ b/plugins/multisrc/novelfire/sources.json @@ -0,0 +1,20 @@ +[ + { + "id": "novelfire", + "sourceSite": "https://novelfire.net/", + "sourceName": "Novel Fire", + "options": { + "lang": "English", + "minorVer": 4, + "versionIncrement": 3 + } + }, + { + "id": "novelphoenix", + "sourceSite": "https://novelphoenix.com/", + "sourceName": "Novel Phoenix", + "options": { + "lang": "English" + } + } +] diff --git a/plugins/english/novelfire.ts b/plugins/multisrc/novelfire/template.ts similarity index 68% rename from plugins/english/novelfire.ts rename to plugins/multisrc/novelfire/template.ts index 59720bb87..9b9d0f13f 100644 --- a/plugins/english/novelfire.ts +++ b/plugins/multisrc/novelfire/template.ts @@ -2,16 +2,33 @@ 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 { Filters } 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.3'; - icon = 'src/en/novelfire/icon.png'; - site = 'https://novelfire.net/'; +type NovelFireOptions = { + lang?: string; + majorVer?: number; + minorVer?: number; + versionIncrement?: number; +}; + +export type NovelFireMetadata = { + id: string; + sourceSite: string; + sourceName: string; + options?: NovelFireOptions; + filters?: Filters; +}; + +export class NovelFirePlugin implements Plugin.PagePlugin { + id: string; + name: string; + icon: string; + site: string; + version: string; + options?: NovelFireOptions; + filters?: Filters; webStorageUtilized = true; novelList = new Set(); draw = 0; @@ -32,6 +49,19 @@ class NovelFire implements Plugin.PluginBase { singlePage = storage.get('singlePage'); pageLength = storage.get('pageLength'); + constructor(metadata: NovelFireMetadata) { + this.id = metadata.id; + this.name = metadata.sourceName; + this.icon = `multisrc/novelfire/${metadata.id.toLowerCase()}/icon.png`; + this.site = metadata.sourceSite; + const majorVer = metadata.options?.majorVer || 1; + const minorVer = metadata.options?.minorVer || 0; + const versionIncrement = metadata.options?.versionIncrement || 0; + this.version = `${majorVer}.${minorVer}.${versionIncrement}`; + this.options = metadata.options; + this.filters = metadata.filters satisfies Filters; + } + async getCheerio(url: string, search: boolean): Promise { const r = await fetchApi(url); if (!r.ok && search != true) @@ -105,19 +135,31 @@ class NovelFire implements Plugin.PluginBase { const url = this.site + 'search-adv'; const params = new URLSearchParams(); - for (const language of filters.language.value) { + 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('ctgcon', filters?.genre_operator?.value || 'and'); + 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('totalchapter', filters?.chapters?.value || '0'); + params.append('ratcon', filters?.rating_operator?.value || 'min'); + params.append('rating', filters?.rating?.value || '0'); + params.append('status', filters?.status?.value || '-1'); + params.append( + 'sort', + showLatestNovels ? 'date' : filters?.sort?.value || 'rank-top', + ); + params.append('tagcon', filters?.tagcon?.value || 'and'); + for (const tag of filters?.tags?.value || []) { + params.append('tags[]', tag); + } + for (const tag of filters?.tags_excluded?.value || []) { + params.append('tags_excluded[]', tag); + } + if (filters?.author?.value) { + params.append('author', filters.author.value); + } params.append('page', pageNo.toString()); const loadedCheerio = await this.getCheerio( @@ -443,162 +485,8 @@ class NovelFire implements Plugin.PluginBase { page === 1, ); } - - 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') { diff --git a/public/static/src/en/novelfire/icon.png b/public/static/multisrc/novelfire/novelfire/icon.png similarity index 100% rename from public/static/src/en/novelfire/icon.png rename to public/static/multisrc/novelfire/novelfire/icon.png diff --git a/public/static/multisrc/novelfire/novelphoenix/icon.png b/public/static/multisrc/novelfire/novelphoenix/icon.png new file mode 100644 index 000000000..ef0be1ba8 Binary files /dev/null and b/public/static/multisrc/novelfire/novelphoenix/icon.png differ