import fetch from "node-fetch-extra" ; import chalk from "chalk" ; import Fuse from "fuse.js" ; import updateManga from "../util/updateManga" ; import { Chapter , ScraperError , ScraperResponse } from "../types" ; import { Scraper , SearchOptions } from "./types" ; import { getProviderId , isProviderId } from "../routers/manga-page" ; import { error } from "./index" ; import { disallowedGenres } from "../config.json" ; /** This is for `DirectoryItem`, the values there aren't very useful */ export enum Directory { Genres = "g" , Slug = "i" , Title = "s" , OngoingPublish = "ps" , OngoingPrint = "ss" , AlternateTitles = "al" } /** This is what the API holds in the Directory array. Fun. */ export interface DirectoryItem { s : string ; i : string ; o : string ; ss : string ; ps : string ; t : string ; v : string ; vm : string ; y : string ; a : string [ ] ; al : string [ ] ; l : string ; lt : number ; g : string [ ] ; h : boolean ; } const headers = { "user-agent" : "Adolla" } ; /** This is a chapter in mangasee API */ interface ChapterResponse { /** For example, 102280 */ Chapter : string ; /**
     * Chapter type.
     * Some mangas name their chapters, differently, like "quest" or "story".
     * Can also be just "chapter"
     */ Type : string ; /** Date string. Formatted like "yyyy-mm-dd hh:mm:ss" */ Date : string ; /** I don't even know */ ChapterName : string | null ; } export class MangaseeClass extends Scraper { constructor ( ) { super ( ) ; this . provider = "Mangasee" ; this . canSearch = true ; this . nsfw = false ; } public async search ( query , options ? , host = "mangasee123" ) { const { resultCount } = { resultCount : 40 , ... options } ; try { let matchedResults = [ ] ; if ( query === "" ) { const searchUrl = `https://${ host }.com/search/?sort=vm&desc=true&name=${ encodeURIComponent ( query ) }` ; const searchRes = await fetch ( searchUrl , { headers } ) ; const html = await searchRes . text ( ) ; try { const directory = JSON . parse ( html . split ( "vm.Directory = " ) [ 1 ] . split ( "];" ) [ 0 ] + "]" ) ; matchedResults = directory . sort ( ( a , b ) => normalizeNumber ( b . v ) - normalizeNumber ( a . v ) ) . slice ( 0 , resultCount ) ; } catch ( err ) { throw new Error ( "Initial search went wrong" ) ; } } else { const directory = await ( await fetch ( `https://${ host }.com/_search.php` , { headers } ) ) . json ( ) ; const fuse = new Fuse ( directory , { threshold : 0.3 , distance : 200 , keys : [ Directory . Title , Directory . Genres , Directory . AlternateTitles ] } ) ; matchedResults = fuse . search ( query ) . map ( ( result ) => result . item ) . slice ( 0 , resultCount ) ; } const searchResultData = await Promise . all ( matchedResults . map ( ( item ) => updateManga ( "Mangasee" , item [ Directory . Slug ] ) ) ) ; return searchResultData . filter ( ( v ) => v . success ) ; } catch ( err ) { if ( host === "mangasee123" ) { return this . search ( query , options , "manga4life" ) ; } else { return [ ] ; } } } param slug param chapter param season public async scrape ( slug , chapterId = null ) { const maxTimeout = new Promise ( ( resolve ) => { setTimeout ( ( ) => { resolve ( error ( 0 , "This request took too long" ) ) ; } , 15e3 ) ; } ) ; const scraping = this . doScrape ( slug , chapterId ) ; const raceResult = await Promise . race ( [ maxTimeout , scraping ] ) ; if ( raceResult . success === false && raceResult . err === "This request took too long" ) { console . error ( chalk . red ( "[MANGADEX]" ) + ` A request for '${ slug }' at '${ chapterId }' took too long and has timed out` ) ; } return raceResult ; } private async doScrape ( slug , chapterId = null , host = "mangasee123" ) { let season ; let chapter ; if ( chapterId && typeof chapterId === "string" ) { const chapterMatch = chapterId . match ( /(\d*\.?\d+)-(\d*\.?\d+)/ ) ; if ( ! chapterMatch ) { return error ( 403 , "Invalid season chapter string" ) ; } const nums = chapterMatch . map ( ( v ) => Number ( v ) ) ; season = nums [ 1 ] ; chapter = nums [ 2 ] ; } const doErr = ( status , reason ) => { if ( host === "mangasee123" ) { return this . doScrape ( slug , chapterId , "manga4life" ) ; } else { console . error ( `${ status } Throwing error for ${ slug }` ) ; return error ( status , reason ) ; } } ; try { const url = `https://${ host }.com/manga/${ slug }` ; const pageRes = await fetch ( url , { headers } ) ; const html = await pageRes . text ( ) ; if ( ! pageRes . ok || pageRes . url . endsWith ( "undefined" ) || html . includes ( "<title>404 Page Not Found</title>" ) ) { return doErr ( pageRes . status , html ) ; } const title = html . split ( "<h1>" ) [ 1 ] . split ( "</h1>" ) [ 0 ] ; const posterUrl = html . split ( '<meta property="og:image" content="' ) [ 1 ] . split ( '"' ) [ 0 ] ; let alternateTitles = [ ] ; if ( html . includes ( "Alternate Name(s):" ) ) alternateTitles = html . split ( '<span class="mlabel">Alternate Name(s):</span>' ) [ 1 ] . split ( "<" ) [ 0 ] . trim ( ) . split ( ", " ) . filter ( Boolean ) ; const descriptionParagraphs = html . split ( '<span class="mlabel">Description:</span>' ) [ 1 ] . split ( ">" ) [ 1 ] . split ( "</" ) [ 0 ] . trim ( ) . split ( "\n" ) . filter ( Boolean ) ; const chapterData = JSON . parse ( html . split ( "vm.Chapters = " ) [ 1 ] . split ( ";" ) [ 0 ] ) ; const chapters = chapterData . map ( ( ch ) => { const season = Number ( ch . Chapter [ 0 ] ) ; const chapter = normalizeNumber ( ch . Chapter . slice ( 1 ) ) / 10 ; const label = `${ ch . Type } ${ chapter }` ; const date = new Date ( ch . Date ) ; return { season , chapter , label , date , hrefString : `${ season }-${ chapter }` , combined : season * 1e5 + chapter } ; } ) . sort ( ( a , b ) => a . combined - b . combined ) ; const genres = JSON . parse ( html . split ( '"genre": ' ) [ 1 ] . split ( "]," ) [ 0 ] + "]" ) ; const status = html . split ( '<span class="mlabel">Status:</span>' ) [ 1 ] . split ( ">" ) [ 1 ] . split ( " (" ) [ 0 ] . trim ( ) . toLowerCase ( ) ; let chapterImages = [ ] ; if ( season >= 0 && chapter >= 0 ) { const chapterUrl = `https://${ host }.com/read-online/${ slug }-chapter-${ chapter }-index-${ season }.html` ; const chapterRes = await fetch ( chapterUrl , { headers } ) ; const chapterBody = await chapterRes . text ( ) ; const cdnUrl = chapterBody . split ( 'vm.CurPathName = "' ) [ 1 ] . split ( '"' ) [ 0 ] ; const curChapter = JSON . parse ( chapterBody . split ( "vm.CurChapter = " ) [ 1 ] . split ( "};" ) [ 0 ] + "}" ) ; chapterImages = [ ] ; for ( let page = 0 ; page < Number ( curChapter . Page ) ; page ++ ) { const [ chapterNormal , ... chapterModded ] = chapter . toString ( ) . split ( /(\.)/ ) ; const chapterString = `${ chapterNormal . padStart ( 4 , "0" ) }${ chapterModded . join ( "" ) }` ; const directoryString = curChapter . Directory ? `/${ curChapter . Directory }` : "" ; const pageString = ( page + 1 ) . toString ( ) . padStart ( 3 , "0" ) ; chapterImages . push ( `https://${ cdnUrl }/manga/${ slug }${ directoryString }/${ chapterString }-${ pageString }.png` ) ; } } let nsfw = false ; for ( const genre of genres ) { if ( disallowedGenres . includes ( genre . toLowerCase ( ) ) ) nsfw = true ; } const providerId = getProviderId ( this . provider ) ; return { constant : { title , slug , posterUrl , alternateTitles , descriptionParagraphs , genres , nsfw } , data : { chapters , chapterImages , status } , success : true , provider : isProviderId ( providerId ) ? providerId : null } ; } catch ( err ) { return doErr ( - 1 , err ) ; } } } const Mangasee = new MangaseeClass ( ) ; export default Mangasee ; param input function normalizeNumber ( input ) { const normalized = Number ( input . slice ( input . split ( "" ) . findIndex ( ( v ) => v !== "0" ) ) ) ; return normalized ; }