diff --git a/scripts/generateSource.cjs b/scripts/generateSource.cjs index f975dafc..6326284e 100644 --- a/scripts/generateSource.cjs +++ b/scripts/generateSource.cjs @@ -17,6 +17,7 @@ const getLanguageCode = language => { Turkish: 'tr', Chinese: 'cn', Japanese: 'jp', + Vietnamese: 'vi', }; return codes[language]; @@ -70,7 +71,9 @@ try { loadedCheerio('${ source.popularNovels.selectors.novelList }').each(function () { - let novelUrl = loadedCheerio(this).find('a').attr('href'); + let novelUrl = loadedCheerio(this).find('${ + source.popularNovels.selectors.novelUrl + }').attr('href'); if (novelUrl && !isUrlAbsolute(novelUrl)) { novelUrl = baseUrl + novelUrl; @@ -164,7 +167,7 @@ try { }').text().trim(); const releaseDate = ${ source.novelAndChapters.selectors.releaseDate - ? `loadedCheerio(this).find('${source.novelAndChapters.selectors.releaseDate}');` + ? `loadedCheerio(this).find('${source.novelAndChapters.selectors.releaseDate}').text();` : null } @@ -275,7 +278,10 @@ try { }); fs.writeFile( - path.resolve(process.cwd(), `src/sources/en/${source.sourceName}.ts`), + path.resolve( + process.cwd(), + `src/sources/${getLanguageCode(source.lang)}/${source.sourceName}.ts`, + ), formatContent, error => { if (error) { diff --git a/src/screens/browse/SettingsBrowseScreen.js b/src/screens/browse/SettingsBrowseScreen.js index 1fcdbd49..81e4aed5 100644 --- a/src/screens/browse/SettingsBrowseScreen.js +++ b/src/screens/browse/SettingsBrowseScreen.js @@ -38,6 +38,7 @@ const BrowseSettings = ({ navigation }) => { 'Turkish', 'French', 'Portuguese (Brazil)', + 'Vietnamese', ].sort(), [], ); diff --git a/src/sources/sourceManager.ts b/src/sources/sourceManager.ts index 0aca0791..3861b9b9 100644 --- a/src/sources/sourceManager.ts +++ b/src/sources/sourceManager.ts @@ -114,6 +114,7 @@ import ReadFreeNovelScraper from './en/readfreenovel'; import FreeNovelUpdatesScraper from './en/freenovelupdates'; import NovelsCafeScraper from './en/NovelsCafe'; import LightNovelReaderScraper from './en/LightNovelReader'; +import HakoLightNovelScraper from './vi/HakoLightNovel'; interface PopularNovelsResponse { totalPages: number; @@ -242,6 +243,7 @@ export const sourceManager = (sourceId: number): Scraper => { 112: FreeNovelUpdatesScraper, 113: NovelsCafeScraper, 114: LightNovelReaderScraper, + 115: HakoLightNovelScraper, }; return scrapers[sourceId]; diff --git a/src/sources/sources.json b/src/sources/sources.json index 5ce2a8f5..b56ece43 100644 --- a/src/sources/sources.json +++ b/src/sources/sources.json @@ -768,5 +768,12 @@ "icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/en/lightnovelreader/icon.png?raw=true", "url": "https://lightnovelreader.org", "lang": "English" + }, + { + "sourceId": 115, + "sourceName": "HakoLightNovel", + "icon": "https://github.com/LNReader/lnreader-sources/blob/main/src/vi/hakolightnovel/icon.png?raw=true", + "url": "https://ln.hako.re", + "lang": "Vietnamese" } ] diff --git a/src/sources/vi/HakoLightNovel.ts b/src/sources/vi/HakoLightNovel.ts new file mode 100644 index 00000000..7fdd55bd --- /dev/null +++ b/src/sources/vi/HakoLightNovel.ts @@ -0,0 +1,196 @@ +import * as cheerio from 'cheerio'; +import { isUrlAbsolute } from '../../utils/isAbsoluteUrl'; +import { + SourceChapter, + SourceChapterItem, + SourceNovel, + SourceNovelItem, +} from '../types'; + +const sourceId = 115; +const sourceName = 'HakoLightNovel'; +const baseUrl = 'https://ln.hako.re'; + +const popularNovels = async (page: number) => { + const totalPages = 49; + const url = `${baseUrl}/danh-sach?page=${page}`; + + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = cheerio.load(body); + + const novels: SourceNovelItem[] = []; + + loadedCheerio('main.row > .thumb-item-flow').each(function () { + let novelUrl = loadedCheerio(this) + .find('div.thumb_attr.series-title > a') + .attr('href'); + + if (novelUrl && !isUrlAbsolute(novelUrl)) { + novelUrl = baseUrl + novelUrl; + } + + if (novelUrl) { + const novelName = loadedCheerio(this).find('.series-title').text().trim(); + let novelCover = loadedCheerio(this).find('img').attr('src'); + + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = baseUrl + novelCover; + } + + const novel = { + sourceId, + novelUrl, + novelName, + novelCover, + }; + + novels.push(novel); + } + }); + + return { totalPages, novels }; +}; + +const parseNovelAndChapters = async (novelUrl: string) => { + const url = novelUrl; + + const result = await fetch(url); + const body = await result.text(); + + let loadedCheerio = cheerio.load(body); + + const novel: SourceNovel = { + sourceId, + sourceName, + url: novelUrl, + novelUrl, + chapters: [], + }; + + novel.novelName = loadedCheerio('.series-name').text(); + + let novelCover = loadedCheerio('.img > img').attr('src'); + + novel.novelCover = novelCover + ? isUrlAbsolute(novelCover) + ? novelCover + : baseUrl + novelCover + : undefined; + + novel.summary = loadedCheerio('.summary-content').text().trim(); + + novel.author = loadedCheerio( + '#mainpart > div:nth-child(2) > div > div:nth-child(1) > section > main > div.top-part > div > div.col-12.col-md-9 > div.series-information > div:nth-child(2) > span.info-value > a', + ) + .text() + .trim(); + + novel.genre = loadedCheerio('.series-gernes') + .text() + .trim() + .replace(/[\t\n ]+/g, ','); + + novel.status = loadedCheerio( + '#mainpart > div:nth-child(2) > div > div:nth-child(1) > section > main > div.top-part > div > div.col-12.col-md-9 > div.series-information > div:nth-child(4) > span.info-value > a', + ) + .text() + .trim(); + + loadedCheerio('.list-chapters li').each(function () { + let chapterUrl = loadedCheerio(this).find('a').attr('href'); + + if (chapterUrl && !isUrlAbsolute(chapterUrl)) { + chapterUrl = baseUrl + chapterUrl; + } + + if (chapterUrl) { + const chapterName = loadedCheerio(this) + .find('.chapter-name') + .text() + .trim(); + const releaseDate = loadedCheerio(this).find('.chapter-time').text(); + + const chapter: SourceChapterItem = { + chapterName, + releaseDate, + chapterUrl, + }; + + novel.chapters?.push(chapter); + } + }); + + return novel; +}; + +const parseChapter = async (novelUrl: string, chapterUrl: string) => { + const url = chapterUrl; + + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = cheerio.load(body); + + const chapterName = loadedCheerio('.title-item').text(); + const chapterText = loadedCheerio('#chapter-content').html() || ''; + + const chapter: SourceChapter = { + sourceId, + novelUrl, + chapterUrl, + chapterName, + chapterText, + }; + + return chapter; +}; + +const searchNovels = async (searchTerm: string) => { + const url = `${baseUrl}/tim-kiem?keywords=${searchTerm}`; + + const result = await fetch(url); + const body = await result.text(); + + const loadedCheerio = cheerio.load(body); + + const novels: SourceNovelItem[] = []; + + loadedCheerio('main.row > .thumb-item-flow').each(function () { + let novelUrl = loadedCheerio(this) + .find('div.thumb_attr.series-title > a') + .attr('href'); + + if (novelUrl && !isUrlAbsolute(novelUrl)) { + novelUrl = baseUrl + novelUrl; + } + + if (novelUrl) { + const novelName = loadedCheerio(this).find('.series-title').text(); + let novelCover = loadedCheerio(this).find('img').attr('src'); + + if (novelCover && !isUrlAbsolute(novelCover)) { + novelCover = baseUrl + novelCover; + } + + novels.push({ + sourceId, + novelUrl, + novelName, + novelCover, + }); + } + }); + + return novels; +}; + +const HakoLightNovelScraper = { + popularNovels, + parseNovelAndChapters, + parseChapter, + searchNovels, +}; + +export default HakoLightNovelScraper;