From 96fc73f17f7b578d0a22c976f45dcf510e1a6a3b Mon Sep 17 00:00:00 2001 From: hammyo-o Date: Tue, 19 Nov 2024 16:09:38 -0600 Subject: [PATCH 1/4] Added skip read manga option and popular sorting --- src/NHentai/NHentai.ts | 61 +++++++++++++++++++++++++++++--- src/NHentai/NHentaiHelper.ts | 6 ++++ src/NHentai/NHentaiInterfaces.ts | 4 +-- src/NHentai/NHentaiSettings.ts | 8 +++++ 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/src/NHentai/NHentai.ts b/src/NHentai/NHentai.ts index cba262a..b02f893 100644 --- a/src/NHentai/NHentai.ts +++ b/src/NHentai/NHentai.ts @@ -84,7 +84,7 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP stateManager = App.createSourceStateManager() - // Sourrce Settings + // Source Settings async getSourceMenu(): Promise { return Promise.resolve(App.createDUISection({ id: 'main', @@ -132,9 +132,20 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP this.CloudFlareError(response.status) const jsonData = this.parseJson(response) + + // Add the manga ID to the read list + await this.addToReadMangaIds(mangaId) + return parseChapterDetails(jsonData, mangaId) } + // Method to store read manga IDs + async addToReadMangaIds(mangaId: string): Promise { + const readMangaIds = await this.stateManager.retrieve('read_manga_ids') ?? {} + readMangaIds[`read_manga_${mangaId}`] = true + await this.stateManager.store('read_manga_ids', readMangaIds) + } + async getSearchTags(): Promise { const arrayTags: Tag[] = [] @@ -150,6 +161,8 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP async getSearchResults(query: SearchRequest, metadata: any): Promise { const page: number = metadata?.page ?? 1 const title: string = query.title ?? '' + const skipReadManga = await this.stateManager.retrieve('skip_read_manga') ?? false + const readMangaIds = skipReadManga ? await this.getReadMangaIds() : [] if (metadata?.stopSearch ?? false) { return App.createPagedResults({ @@ -178,7 +191,7 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP } }) - // Normal search query + // Normal search query } else { const q: string = encodeURIComponent(`${title} ${query?.includedTags?.map((x: Tag) => ` +${x.id}`)} `) + await this.generateQuery() @@ -190,8 +203,9 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP this.CloudFlareError(response.status) const jsonData = this.parseJson(response) + const results = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) return App.createPagedResults({ - results: parseSearch(jsonData), + results, metadata: { page: page + 1 } @@ -200,6 +214,8 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP } async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise { + const skipReadManga = await this.stateManager.retrieve('skip_read_manga') ?? false + const readMangaIds = skipReadManga ? await this.getReadMangaIds() : [] const sections = [ { request: App.createRequest({ @@ -236,6 +252,30 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP containsMoreItems: true, type: HomeSectionType.singleRowNormal }) + }, + { + request: App.createRequest({ + url: `${NHENTAI_URL}/api/galleries/search?query=${await this.generateQuery()}&sort=popular-month`, + method: 'GET' + }), + sectionID: App.createHomeSection({ + id: 'popular-month', + title: 'Popular Monthly', + containsMoreItems: true, + type: HomeSectionType.singleRowNormal + }) + }, + { + request: App.createRequest({ + url: `${NHENTAI_URL}/api/galleries/search?query=${await this.generateQuery()}&sort=popular`, + method: 'GET' + }), + sectionID: App.createHomeSection({ + id: 'popular', + title: 'Popular All-Time', + containsMoreItems: true, + type: HomeSectionType.singleRowNormal + }) } ] @@ -251,7 +291,7 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP if (hasNoResults(jsonData)) { return } - section.sectionID.items = parseSearch(jsonData) + section.sectionID.items = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) sectionCallback(section.sectionID) }) @@ -263,6 +303,8 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP async getViewMoreItems(homepageSectionId: string, metadata: any): Promise { let page: number = metadata?.page ?? 1 + const skipReadManga = await this.stateManager.retrieve('skip_read_manga') ?? false + const readMangaIds = skipReadManga ? await this.getReadMangaIds() : [] const request = App.createRequest({ url: `${NHENTAI_URL}/api/galleries/search?query=${await this.generateQuery()}&sort=${homepageSectionId}&page=${page}`, method: 'GET' @@ -273,14 +315,23 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP const jsonData = this.parseJson(response) page++ + const results = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) return App.createPagedResults({ - results: parseSearch(jsonData), + results, metadata: { page: page } }) } + async getReadMangaIds(): Promise { + const allData = await this.stateManager.retrieve('read_manga_ids') + if (!allData) { + return [] + } + return Object.keys(allData).filter(key => key.startsWith('read_manga_')).map(key => key.replace('read_manga_', '')) + } + CloudFlareError(status: number): void { if (status == 503 || status == 403) { throw new Error(`CLOUDFLARE BYPASS ERROR:\nPlease go to the homepage of <${NHentai.name}> and press the cloud icon.`) diff --git a/src/NHentai/NHentaiHelper.ts b/src/NHentai/NHentaiHelper.ts index 4d3d1d5..aa52681 100644 --- a/src/NHentai/NHentaiHelper.ts +++ b/src/NHentai/NHentaiHelper.ts @@ -87,6 +87,12 @@ class NHSortOrderClass { name: 'Most Recent', NHCode: 'date', shortcuts: ['s:r', 's:recent', 'sort:r', 'sort:recent'] + }, + { + // Sort by popular this month + name: 'Popular This Month', + NHCode: 'popular-month', + shortcuts: ['s:pm', 's:m', 's:popular-month', 'sort:pm', 'sort:m', 'sort:popular-month'] } ] diff --git a/src/NHentai/NHentaiInterfaces.ts b/src/NHentai/NHentaiInterfaces.ts index 5a53201..ce921b1 100644 --- a/src/NHentai/NHentaiInterfaces.ts +++ b/src/NHentai/NHentaiInterfaces.ts @@ -1,6 +1,6 @@ export interface ImagePageObject { - t: 'j' | 'p' | 'g';// JPG (≧◡≦) + t: 'j' | 'p' | 'g' | 'w'; // Added 'w' for webp w: number; h: number; } @@ -107,5 +107,5 @@ export interface QueryResponse { export interface RequestMetadata { nextPage?: number; maxPages?: number; - sort: 'popular-today' | 'popular-week' | 'popular' | ''; + sort: 'popular-today' | 'popular-week' | 'popular-month' | 'popular' | ''; } \ No newline at end of file diff --git a/src/NHentai/NHentaiSettings.ts b/src/NHentai/NHentaiSettings.ts index 23e21d6..378d6ca 100644 --- a/src/NHentai/NHentaiSettings.ts +++ b/src/NHentai/NHentaiSettings.ts @@ -75,6 +75,14 @@ export const settings = (stateManager: SourceStateManager): DUINavigationButton ) } }) + }), + App.createDUISwitch({ + id: 'skip_read_manga', + label: 'Skip Read Manga', + value: App.createDUIBinding({ + get: async () => await stateManager.retrieve('skip_read_manga') ?? false, + set: async (newValue) => await stateManager.store('skip_read_manga', newValue) + }) }) ] }, From 74cf02ca86f20fdbf03d894d0008c8cff2ffd681 Mon Sep 17 00:00:00 2001 From: hammyo-o Date: Tue, 26 Nov 2024 12:31:08 -0600 Subject: [PATCH 2/4] min pages, listed pages nd fave count --- src/NHentai/NHentai.ts | 83 ++++++++++++---------------------- src/NHentai/NHentaiParser.ts | 20 +++++--- src/NHentai/NHentaiSettings.ts | 19 ++++++-- 3 files changed, 58 insertions(+), 64 deletions(-) diff --git a/src/NHentai/NHentai.ts b/src/NHentai/NHentai.ts index b02f893..b686a7c 100644 --- a/src/NHentai/NHentai.ts +++ b/src/NHentai/NHentai.ts @@ -29,7 +29,8 @@ import { parseMangaDetails, parseChapters, parseChapterDetails, - parseSearch + parseSearch, + addToReadMangaIds } from './NHentaiParser' import { @@ -43,7 +44,7 @@ import { popularTags } from './tags.json' const NHENTAI_URL = 'https://nhentai.net' export const NHentaiInfo: SourceInfo = { - version: '4.0.8', + version: '4.0.9', name: 'nhentai', icon: 'icon.png', author: 'NotMarek & Netsky', @@ -134,18 +135,11 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP const jsonData = this.parseJson(response) // Add the manga ID to the read list - await this.addToReadMangaIds(mangaId) + await addToReadMangaIds(this.stateManager, mangaId) return parseChapterDetails(jsonData, mangaId) } - // Method to store read manga IDs - async addToReadMangaIds(mangaId: string): Promise { - const readMangaIds = await this.stateManager.retrieve('read_manga_ids') ?? {} - readMangaIds[`read_manga_${mangaId}`] = true - await this.stateManager.store('read_manga_ids', readMangaIds) - } - async getSearchTags(): Promise { const arrayTags: Tag[] = [] @@ -173,44 +167,23 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP }) } - // When given number query - if (/^\d+$/.test(title)) { - const request = App.createRequest({ - url: `${NHENTAI_URL}/api/gallery/${title}`, - method: 'GET' - }) - const response = await this.requestManager.schedule(request, 1) - this.CloudFlareError(response.status) - - const jsonData = this.parseJson(response) - return App.createPagedResults({ - results: parseSearch({ result: [jsonData], num_pages: 1, per_page: 1 }), - metadata: { - page: page + 1, - stopSearch: true - } - }) - - // Normal search query - } else { - const q: string = encodeURIComponent(`${title} ${query?.includedTags?.map((x: Tag) => ` +${x.id}`)} `) + await this.generateQuery() + const q: string = encodeURIComponent(`${title} ${query?.includedTags?.map((x: Tag) => ` +${x.id}`)} `) + await this.generateQuery() + const request = App.createRequest({ + url: `${NHENTAI_URL}/api/galleries/search?query=${q}&page=${page}&sort=${await this.sortOrder(this.stateManager)}`, + method: 'GET' + }) + const response = await this.requestManager.schedule(request, 1) + this.CloudFlareError(response.status) - const request = App.createRequest({ - url: `${NHENTAI_URL}/api/galleries/search?query=${(q)}&page=${page}&sort=${await this.sortOrder(this.stateManager)}`, - method: 'GET' - }) - const response = await this.requestManager.schedule(request, 1) - this.CloudFlareError(response.status) + const jsonData = this.parseJson(response) + const results = parseSearch(jsonData, readMangaIds ?? []) - const jsonData = this.parseJson(response) - const results = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) - return App.createPagedResults({ - results, - metadata: { - page: page + 1 - } - }) - } + return App.createPagedResults({ + results, + metadata: { + page: page + 1 // Increment by one page + } + }) } async getHomePageSections(sectionCallback: (section: HomeSection) => void): Promise { @@ -291,8 +264,7 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP if (hasNoResults(jsonData)) { return } - section.sectionID.items = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) - + section.sectionID.items = parseSearch(jsonData, readMangaIds ?? []) sectionCallback(section.sectionID) }) ) @@ -305,17 +277,18 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP let page: number = metadata?.page ?? 1 const skipReadManga = await this.stateManager.retrieve('skip_read_manga') ?? false const readMangaIds = skipReadManga ? await this.getReadMangaIds() : [] + const request = App.createRequest({ url: `${NHENTAI_URL}/api/galleries/search?query=${await this.generateQuery()}&sort=${homepageSectionId}&page=${page}`, method: 'GET' }) - const response = await this.requestManager.schedule(request, 1) this.CloudFlareError(response.status) + const jsonData = this.parseJson(response) + const results = parseSearch(jsonData, readMangaIds ?? []) - page++ - const results = parseSearch(jsonData).filter(manga => !readMangaIds.includes(manga.mangaId)) + page += 1 // Increment by one page return App.createPagedResults({ results, metadata: { @@ -329,7 +302,7 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP if (!allData) { return [] } - return Object.keys(allData).filter(key => key.startsWith('read_manga_')).map(key => key.replace('read_manga_', '')) + return Object.keys(allData).map(key => key) } CloudFlareError(status: number): void { @@ -360,8 +333,10 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP } async generateQuery(): Promise { - const query = await this.language(this.stateManager) + await this.extraArgs(this.stateManager) - return encodeURIComponent(query) + const langQuery = await this.language(this.stateManager) + const extraArgs = await this.extraArgs(this.stateManager) + const minPages = await this.stateManager.retrieve('min_pages') ?? 0 + return encodeURIComponent(`${langQuery} ${extraArgs} pages:>${minPages}`) } async language(stateManager: SourceStateManager): Promise { diff --git a/src/NHentai/NHentaiParser.ts b/src/NHentai/NHentaiParser.ts index 84ef906..b51394b 100644 --- a/src/NHentai/NHentaiParser.ts +++ b/src/NHentai/NHentaiParser.ts @@ -3,7 +3,8 @@ import { ChapterDetails, PartialSourceManga, Tag, - Chapter + Chapter, + SourceStateManager } from '@paperback/types' import { NHLanguages } from './NHentaiHelper' @@ -34,7 +35,7 @@ export const parseMangaDetails = (data: Gallery): SourceManga => { image: `https://t.nhentai.net/galleries/${data.media_id}/cover.${typeOfImage(data.images.cover)}`, status: 'Completed', tags: [App.createTagSection({ id: 'tags', label: 'Tags', tags: tags })], - desc: `Pages: ${data.num_pages}` + desc: `Pages: ${data.num_pages} | Favorites: ${data.num_favorites}` }) }) } @@ -60,29 +61,34 @@ export const parseChapterDetails = (data: Gallery, mangaId: string): ChapterDeta }) } -export const parseSearch = (data: QueryResponse): PartialSourceManga[] => { +export const parseSearch = (data: QueryResponse, readMangaIds: string[]): PartialSourceManga[] => { const tiles: PartialSourceManga[] = [] const collectedIds: string[] = [] if (!data?.result) { console.log(JSON.stringify(data)) - throw new Error('JSON NO RESULT ERROR!\n\nYou\'ve like set too many additional arguments in this source\'s settings, remove some to see results!\nSo search with tags you need to use arguments like shown in the sourc\'s settings!') + throw new Error('JSON NO RESULT ERROR!\n\nYou\'ve like set too many additional arguments in this source\'s settings, remove some to see results!\nSo search with tags you need to use arguments like shown in the source\'s settings!') } for (const gallery of data.result) { - - if (collectedIds.includes(gallery.id.toString())) continue + if (collectedIds.includes(gallery.id.toString()) || readMangaIds.includes(gallery.id.toString())) continue tiles.push(App.createPartialSourceManga({ image: `https://t.nhentai.net/galleries/${gallery.media_id}/cover.${typeOfImage(gallery.images.cover)}`, title: gallery.title.pretty, mangaId: gallery.id.toString(), - subtitle: NHLanguages.getName(getLanguage(gallery)) + subtitle: NHLanguages.getName(getLanguage(gallery)).substring(0, 3) + ' | Pgs: ' + gallery.num_pages })) collectedIds.push(gallery.id.toString()) } return tiles } +export const addToReadMangaIds = async (stateManager: SourceStateManager, mangaId: string): Promise => { + const readMangaIds = await stateManager.retrieve('read_manga_ids') ?? {} + readMangaIds[mangaId] = true + await stateManager.store('read_manga_ids', readMangaIds) +} + // Utility function capitalizeTags(str: string) { return str.split(' ').map(word => { diff --git a/src/NHentai/NHentaiSettings.ts b/src/NHentai/NHentaiSettings.ts index 378d6ca..a7645af 100644 --- a/src/NHentai/NHentaiSettings.ts +++ b/src/NHentai/NHentaiSettings.ts @@ -21,6 +21,9 @@ export const getExtraArgs = async (stateManager: SourceStateManager): Promise => { + return (await stateManager.retrieve('min_pages') as number) ?? 0 +} export const settings = (stateManager: SourceStateManager): DUINavigationButton => { return App.createDUINavigationButton({ @@ -36,9 +39,10 @@ export const settings = (stateManager: SourceStateManager): DUINavigationButton await Promise.all([ getLanguages(stateManager), getSortOrders(stateManager), - getExtraArgs(stateManager) + getExtraArgs(stateManager), + getMinPages(stateManager) ]) - return await [ + return [ App.createDUISelect({ id: 'languages', label: 'Languages', @@ -83,6 +87,14 @@ export const settings = (stateManager: SourceStateManager): DUINavigationButton get: async () => await stateManager.retrieve('skip_read_manga') ?? false, set: async (newValue) => await stateManager.store('skip_read_manga', newValue) }) + }), + App.createDUIInputField({ + id: 'min_pages', + label: 'Minimum Pages', + value: App.createDUIBinding({ + get: () => getMinPages(stateManager), + set: async (newValue: number) => await stateManager.store('min_pages', newValue) + }) }) ] }, @@ -102,7 +114,8 @@ export const resetSettings = (stateManager: SourceStateManager): DUIButton => { await Promise.all([ stateManager.store('languages', null), stateManager.store('sort_order', null), - stateManager.store('extra_args', null) + stateManager.store('extra_args', null), + stateManager.store('min_pages', null) ]) } }) From 1af18ac14415e883ac5f0cb31cec3bcc840790a1 Mon Sep 17 00:00:00 2001 From: hammyo-o Date: Tue, 26 Nov 2024 12:59:12 -0600 Subject: [PATCH 3/4] Refactor query generation to conditionally include minimum pages --- src/NHentai/NHentai.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/NHentai/NHentai.ts b/src/NHentai/NHentai.ts index b686a7c..cb6a58a 100644 --- a/src/NHentai/NHentai.ts +++ b/src/NHentai/NHentai.ts @@ -335,8 +335,9 @@ export class NHentai implements SearchResultsProviding, MangaProviding, ChapterP async generateQuery(): Promise { const langQuery = await this.language(this.stateManager) const extraArgs = await this.extraArgs(this.stateManager) - const minPages = await this.stateManager.retrieve('min_pages') ?? 0 - return encodeURIComponent(`${langQuery} ${extraArgs} pages:>${minPages}`) + const minPages = (await this.stateManager.retrieve('min_pages')) ?? 0 + const minPagesQuery = minPages ? `pages:>${minPages}` : '' + return encodeURIComponent(`${langQuery} ${extraArgs} ${minPagesQuery}`.trim()) } async language(stateManager: SourceStateManager): Promise { From d1c15d22a13ded9200e6b4102eeae2d3d06cf74f Mon Sep 17 00:00:00 2001 From: hammyo-o Date: Mon, 9 Dec 2024 09:45:29 -0600 Subject: [PATCH 4/4] NHentai | Fixed broken img URLS caused by their changed website prefixes. --- src/NHentai/NHentaiParser.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/NHentai/NHentaiParser.ts b/src/NHentai/NHentaiParser.ts index b51394b..9142403 100644 --- a/src/NHentai/NHentaiParser.ts +++ b/src/NHentai/NHentaiParser.ts @@ -32,7 +32,7 @@ export const parseMangaDetails = (data: Gallery): SourceManga => { titles: Object.values(data.title).filter(title => title !== null), artist: artist, author: artist, - image: `https://t.nhentai.net/galleries/${data.media_id}/cover.${typeOfImage(data.images.cover)}`, + image: getThumbnailUrl(data.media_id, data.images.cover), status: 'Completed', tags: [App.createTagSection({ id: 'tags', label: 'Tags', tags: tags })], desc: `Pages: ${data.num_pages} | Favorites: ${data.num_favorites}` @@ -55,8 +55,7 @@ export const parseChapterDetails = (data: Gallery, mangaId: string): ChapterDeta id: mangaId, mangaId: mangaId, pages: data.images.pages.map((image, i) => { - const type = typeOfImage(image) - return `https://i.nhentai.net/galleries/${data.media_id}/${i + 1}.${type}` + return getPageUrl(data.media_id, i + 1, image) }) }) } @@ -73,7 +72,7 @@ export const parseSearch = (data: QueryResponse, readMangaIds: string[]): Partia for (const gallery of data.result) { if (collectedIds.includes(gallery.id.toString()) || readMangaIds.includes(gallery.id.toString())) continue tiles.push(App.createPartialSourceManga({ - image: `https://t.nhentai.net/galleries/${gallery.media_id}/cover.${typeOfImage(gallery.images.cover)}`, + image: getThumbnailUrl(gallery.media_id, gallery.images.cover), title: gallery.title.pretty, mangaId: gallery.id.toString(), subtitle: NHLanguages.getName(getLanguage(gallery)).substring(0, 3) + ' | Pgs: ' + gallery.num_pages @@ -93,7 +92,7 @@ export const addToReadMangaIds = async (stateManager: SourceStateManager, mangaI function capitalizeTags(str: string) { return str.split(' ').map(word => { return word.charAt(0).toUpperCase() + word.slice(1) - }).join(' ') + }).join(' ') } const typeMap: { [key: string]: string; } = { 'j': 'jpg', 'p': 'png', 'g': 'gif', 'w': 'webp'} @@ -102,6 +101,14 @@ const typeOfImage = (image: ImagePageObject): string => { return typeMap[image.t] ?? '' } +const getThumbnailUrl = (mediaId: string, image: ImagePageObject): string => { + return `https://t3.nhentai.net/galleries/${mediaId}/cover.${typeOfImage(image)}` +} + +const getPageUrl = (mediaId: string, pageNumber: number, image: ImagePageObject): string => { + return `https://i4.nhentai.net/galleries/${mediaId}/${pageNumber}.${typeOfImage(image)}` +} + const getArtist = (gallery: Gallery): string => { const tags: TagObject[] = gallery.tags for (const tag of tags) {