From 2a1e4dd4a97c5c92de4bc348a34ace30cd9f682c Mon Sep 17 00:00:00 2001 From: koikiss-dev Date: Sat, 2 Mar 2024 16:26:12 -0600 Subject: [PATCH 01/64] add provider model --- .../v1/anime/animeflv/AnimeflvRoutes.ts | 13 +- .../v1/anime/gogoanime/GogoAnimeRoute.ts | 3 +- src/scraper/ScraperAnimeModel.ts | 15 + .../sites/anime/AnimeBlix/AnimeBlix.ts | 395 ++++++++++-------- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 19 +- .../anime/animelatinohd/AnimeLatinoHD.ts | 324 +++++++------- .../sites/anime/gogoanime/Gogoanime.ts | 3 +- .../sites/anime/wcostream/WcoStream.ts | 390 ++++++++++------- src/scraper/sites/anime/zoro/Zoro.ts | 18 +- 9 files changed, 704 insertions(+), 476 deletions(-) create mode 100644 src/scraper/ScraperAnimeModel.ts diff --git a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts index cee1e705..3a5bad96 100644 --- a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts +++ b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts @@ -8,7 +8,6 @@ import { } from "../../../../scraper/sites/anime/animeflv/animeflv_helper"; const r = Router(); - //anime info r.get("/anime/flv/name/:name", async (req, res) => { try { @@ -34,7 +33,7 @@ r.get("/anime/flv/episode/:episode", async (req, res) => { res.status(500).send(error); } }); - + //filter r.get("/anime/flv/filter", async (req, res) => { try { @@ -47,7 +46,15 @@ r.get("/anime/flv/filter", async (req, res) => { const title = req.query.title as string; const flv = new AnimeFlv(); - const animeInfo = await flv.Filter(gen, date, type, status, ord, page, title); + const animeInfo = await flv.GetAnimeByFilter( + gen, + date, + type, + status, + ord, + page, + title + ); res.send(animeInfo); } catch (error) { console.log(error); diff --git a/src/routes/v1/anime/gogoanime/GogoAnimeRoute.ts b/src/routes/v1/anime/gogoanime/GogoAnimeRoute.ts index a745d1e7..df6e1657 100644 --- a/src/routes/v1/anime/gogoanime/GogoAnimeRoute.ts +++ b/src/routes/v1/anime/gogoanime/GogoAnimeRoute.ts @@ -1,4 +1,4 @@ -import { Router } from "express"; +/* import { Router } from "express"; import { GogoanimeInfo, GogoanimeServer, @@ -63,3 +63,4 @@ r.get("/anime/gogoanime/filter", async (req, res) => { }); export default r; + */ diff --git a/src/scraper/ScraperAnimeModel.ts b/src/scraper/ScraperAnimeModel.ts new file mode 100644 index 00000000..bdaefff0 --- /dev/null +++ b/src/scraper/ScraperAnimeModel.ts @@ -0,0 +1,15 @@ +import { Anime } from "../types/anime"; +import { IResultSearch, IAnimeSearch } from "../types/search"; +import { Episode } from "../types/episode"; + +export abstract class AnimeProviderModel { + abstract readonly url: string; + + abstract GetAnimeInfo(anime: string): Promise; + + abstract GetAnimeByFilter( + ...args: any[] + ): Promise>; + + abstract GetEpisodeServers(...args: any[]): Promise; +} diff --git a/src/scraper/sites/anime/AnimeBlix/AnimeBlix.ts b/src/scraper/sites/anime/AnimeBlix/AnimeBlix.ts index ef30e473..ecdaf77f 100644 --- a/src/scraper/sites/anime/AnimeBlix/AnimeBlix.ts +++ b/src/scraper/sites/anime/AnimeBlix/AnimeBlix.ts @@ -2,181 +2,240 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "@animetypes/anime"; import { Episode, EpisodeServer } from "@animetypes/episode"; -import { AnimeSearch, ResultSearch, IResultSearch, IAnimeSearch } from "@animetypes/search"; +import { + AnimeSearch, + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "@animetypes/search"; +import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains * https://vwv.animeblix.org - * + * * https://animeblix.xyz - * + * * https://animeblix.com - * -*/ - -export class AnimeBlix { - readonly url = "https://vwv.animeblix.org"; - readonly api = "https://api.animelatinohd.com"; - - async GetAnimeInfo(anime: string): Promise { - try { - const { data } = await axios.get(`${this.url}/animes/${anime.includes("ver-") ? anime : "ver-"+anime}`); - const $ = cheerio.load(data); - - const AnimeTypes = $(".cn .info .r .u li span:contains('Tipo:')").next() - const AnimeStatus = $(".cn .info .r .u li span[class='em']").length ? $(".cn .info .r .u li span[class='em']").text() : $(".cn .info .r .u li span[class='fi']").length ? $(".cn .info .r .u li span[class='fi']").text() : $(".cn .info .r .u li span[class='es']").text() - const AnimeDate = $(".cn .info .r .u li span:contains('Fecha de emisión:')").next().text().trim().replace(" -", "").split(" ") - - const Dates = AnimeDate[0] ? new Date(String(AnimeDate[0])) : null - const DateFormat = new Intl.DateTimeFormat("en", { day: "numeric", month: "numeric", year: "numeric" }).format(Dates).split("/") - - - const AcceptAlts = $(".cn .info .r .u").next().find("li").text().replace("Nombre original: ", "").replace("Nombre en inglés: ", "---").replace("Nombre en japones: ", "---") - - let AltsSlice: number = 0 - - if (AcceptAlts.includes("Támbien conocido como:")) { - AltsSlice = AcceptAlts.indexOf("Támbien conocido como:") - } else if (AcceptAlts.includes("Estudio(s):")) { - AltsSlice = AcceptAlts.indexOf("Estudio(s):") - } else if (AcceptAlts.includes("Producido por:")) { - AltsSlice = AcceptAlts.indexOf("Producido por:") - } else if (AcceptAlts.includes("Licenciada por:")) { - AltsSlice = AcceptAlts.indexOf("Licenciada por:") - } - - const AltNames = AcceptAlts.slice(0, AltsSlice) - const AnimeInfo: Anime = { - name: $(".cn .ti h1 strong").text(), - url: `/anime/animeblix/name/${anime}`, - synopsis: $(".cn .info .r .tx .content p").first().text(), - alt_name: [...AltNames.split("---")], - image: { - url: $(".cn .info .l .i img").attr("data-src") - }, - genres: [...$(".cn .info .r .gn li").text().split(",")], - type: AnimeTypes.length ? AnimeTypes.text() == "TV" ? "Anime" : AnimeTypes.text() == "Pelicula" ? "Movie" : AnimeTypes.text() == "Ova" ? "OVA" : "Null" : "Null", //tv,pelicula,especial,ova - status: AnimeStatus, - date: AnimeDate[0] ? { year: DateFormat[2], month: DateFormat[1], day: DateFormat[0] } : null, - episodes: [] - } - - const ListEpisodeIndex = $(".sc .cn #l").html() - const RemoveSymbols: RegExp = /[^0-9,]+/g; - const ReplaceSymbols: RegExp = /(,)+/g; - const ListEpisode = ListEpisodeIndex.slice(ListEpisodeIndex.indexOf("var eps = "), ListEpisodeIndex.indexOf("; { - const AnimeEpisode: Episode = { - name: "Episode " +e, - number: e, - image: "", - url: `/anime/animeblix/episode/${anime+"-"+e}` - } - - AnimeInfo.episodes.push(AnimeEpisode); - }) - - return AnimeInfo; - - } catch (error) { - console.log(error) - } + * + */ + +export class AnimeBlix extends AnimeProviderModel { + readonly url = "https://vwv.animeblix.org"; + readonly api = "https://api.animelatinohd.com"; + + async GetAnimeInfo(anime: string): Promise { + try { + const { data } = await axios.get( + `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}` + ); + const $ = cheerio.load(data); + + const AnimeTypes = $(".cn .info .r .u li span:contains('Tipo:')").next(); + const AnimeStatus = $(".cn .info .r .u li span[class='em']").length + ? $(".cn .info .r .u li span[class='em']").text() + : $(".cn .info .r .u li span[class='fi']").length + ? $(".cn .info .r .u li span[class='fi']").text() + : $(".cn .info .r .u li span[class='es']").text(); + const AnimeDate = $( + ".cn .info .r .u li span:contains('Fecha de emisión:')" + ) + .next() + .text() + .trim() + .replace(" -", "") + .split(" "); + + const Dates = AnimeDate[0] ? new Date(String(AnimeDate[0])) : null; + const DateFormat = new Intl.DateTimeFormat("en", { + day: "numeric", + month: "numeric", + year: "numeric", + }) + .format(Dates) + .split("/"); + + const AcceptAlts = $(".cn .info .r .u") + .next() + .find("li") + .text() + .replace("Nombre original: ", "") + .replace("Nombre en inglés: ", "---") + .replace("Nombre en japones: ", "---"); + + let AltsSlice: number = 0; + + if (AcceptAlts.includes("Támbien conocido como:")) { + AltsSlice = AcceptAlts.indexOf("Támbien conocido como:"); + } else if (AcceptAlts.includes("Estudio(s):")) { + AltsSlice = AcceptAlts.indexOf("Estudio(s):"); + } else if (AcceptAlts.includes("Producido por:")) { + AltsSlice = AcceptAlts.indexOf("Producido por:"); + } else if (AcceptAlts.includes("Licenciada por:")) { + AltsSlice = AcceptAlts.indexOf("Licenciada por:"); + } + + const AltNames = AcceptAlts.slice(0, AltsSlice); + const AnimeInfo: Anime = { + name: $(".cn .ti h1 strong").text(), + url: `/anime/animeblix/name/${anime}`, + synopsis: $(".cn .info .r .tx .content p").first().text(), + alt_name: [...AltNames.split("---")], + image: { + url: $(".cn .info .l .i img").attr("data-src"), + }, + genres: [...$(".cn .info .r .gn li").text().split(",")], + type: AnimeTypes.length + ? AnimeTypes.text() == "TV" + ? "Anime" + : AnimeTypes.text() == "Pelicula" + ? "Movie" + : AnimeTypes.text() == "Ova" + ? "OVA" + : "Null" + : "Null", //tv,pelicula,especial,ova + status: AnimeStatus, + date: AnimeDate[0] + ? { year: DateFormat[2], month: DateFormat[1], day: DateFormat[0] } + : null, + episodes: [], + }; + + const ListEpisodeIndex = $(".sc .cn #l").html(); + const RemoveSymbols: RegExp = /[^0-9,]+/g; + const ReplaceSymbols: RegExp = /(,)+/g; + const ListEpisode = ListEpisodeIndex.slice( + ListEpisodeIndex.indexOf("var eps = "), + ListEpisodeIndex.indexOf("; { + const AnimeEpisode: Episode = { + name: "Episode " + e, + number: e, + image: "", + url: `/anime/animeblix/episode/${anime + "-" + e}`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.log(error); } - async GetEpisodeServers(episode: string): Promise { - try { - - const number = episode.substring(episode.lastIndexOf("-") + 1) - const anime = episode.substring(0, episode.lastIndexOf("-")) - - const { data } = await axios.get(`${this.url}/${anime.replace("ver-","")}-${number}`); - const $ = cheerio.load(data); - fetch("https://vwv.animeblix.org/back", { - "headers": { - "accept": "*/*", - "accept-language": "es-419,es;q=0.9,es-ES;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5", - "content-type": "application/x-www-form-urlencoded; charset=UTF-8", - "sec-ch-ua": "\"Not A(Brand\";v=\"99\", \"Microsoft Edge\";v=\"121\", \"Chromium\";v=\"121\"", - "sec-ch-ua-mobile": "?0", - "sec-ch-ua-platform": "\"Windows\"", - "sec-fetch-dest": "empty", - "sec-fetch-mode": "cors", - "sec-fetch-site": "same-origin", - "x-requested-with": "XMLHttpRequest", - "cookie": "dom3ic8zudi28v8lr6fgphwffqoz0j6c=85632e4b-e8b5-4427-be7b-8b3c7156a788%3A1%3A1; sb_main_47d33eb2af15845dedc4fb60a160b2b4=1; sb_count_47d33eb2af15845dedc4fb60a160b2b4=2; sb_onpage_47d33eb2af15845dedc4fb60a160b2b4=0; sb_page_47d33eb2af15845dedc4fb60a160b2b4=7; pp_main_325f99fa973f521d38ec7eea8396403e=1; pp_sub_325f99fa973f521d38ec7eea8396403e=1", - - "Referrer-Policy": "strict-origin-when-cross-origin" - }, - "body": "acc=opt&i=333334322d3132", - "method": "POST" -}).then((e) =>e).then(async(e) =>console.log(await e.text())); - - const AnimeEpisodeInfo: Episode = { - name: number, - url: `/anime/animeblix/episode/${episode}`, - number: number, - image: "", - servers: [] - } - - - $("").map((e) => { - - const Server: EpisodeServer = { - name: "e.server.title", - url: "", - } - - Server.url = "https://api.animelatinohd.com/stream/" - Server.name = String(e) - - AnimeEpisodeInfo.servers.push(Server) - }) - - return AnimeEpisodeInfo; - } catch (error) { - console.log(error) - } + } + async GetEpisodeServers(episode: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + + const { data } = await axios.get( + `${this.url}/${anime.replace("ver-", "")}-${number}` + ); + const $ = cheerio.load(data); + fetch("https://vwv.animeblix.org/back", { + headers: { + accept: "*/*", + "accept-language": + "es-419,es;q=0.9,es-ES;q=0.8,en;q=0.7,en-GB;q=0.6,en-US;q=0.5", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "sec-ch-ua": + '"Not A(Brand";v="99", "Microsoft Edge";v="121", "Chromium";v="121"', + "sec-ch-ua-mobile": "?0", + "sec-ch-ua-platform": '"Windows"', + "sec-fetch-dest": "empty", + "sec-fetch-mode": "cors", + "sec-fetch-site": "same-origin", + "x-requested-with": "XMLHttpRequest", + cookie: + "dom3ic8zudi28v8lr6fgphwffqoz0j6c=85632e4b-e8b5-4427-be7b-8b3c7156a788%3A1%3A1; sb_main_47d33eb2af15845dedc4fb60a160b2b4=1; sb_count_47d33eb2af15845dedc4fb60a160b2b4=2; sb_onpage_47d33eb2af15845dedc4fb60a160b2b4=0; sb_page_47d33eb2af15845dedc4fb60a160b2b4=7; pp_main_325f99fa973f521d38ec7eea8396403e=1; pp_sub_325f99fa973f521d38ec7eea8396403e=1", + + "Referrer-Policy": "strict-origin-when-cross-origin", + }, + body: "acc=opt&i=333334322d3132", + method: "POST", + }) + .then((e) => e) + .then(async (e) => console.log(await e.text())); + + const AnimeEpisodeInfo: Episode = { + name: number, + url: `/anime/animeblix/episode/${episode}`, + number: number, + image: "", + servers: [], + }; + + $("").map((e) => { + const Server: EpisodeServer = { + name: "e.server.title", + url: "", + }; + + Server.url = "https://api.animelatinohd.com/stream/"; + Server.name = String(e); + + AnimeEpisodeInfo.servers.push(Server); + }); + + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); } - - async GetAnimeByFilter(search?: string, type?: number, page?: number, year?: string, genre?: string): Promise> { - try { - const { data } = await axios.get(`${this.api}/api/anime/list`, { - params: { - search: search, - type: type, - year: year, - genre: genre, - page: page - } - }); - - const animeSearchParseObj = data - - const animeSearch: ResultSearch = { - nav: { - count: animeSearchParseObj.data.length, - current: animeSearchParseObj.current_page, - next: animeSearchParseObj.data.length < 28 ? 0 : animeSearchParseObj.current_page + 1, - hasNext: animeSearchParseObj.data.length < 28 ? false : true - }, - results: [] - } - animeSearchParseObj.data.map(e => { - const animeSearchData: AnimeSearch = { - name: e.name, - image: "https://www.themoviedb.org/t/p/original" + e.poster + "?&w=53&q=95", - url: `/anime/animelatinohd/name/${e.slug}`, - type: "" - } - animeSearch.results.push(animeSearchData) - }) - return animeSearch; - } catch (error) { - console.log(error) - } + } + + async GetAnimeByFilter( + search?: string, + type?: number, + page?: number, + year?: string, + genre?: string + ): Promise> { + try { + const { data } = await axios.get(`${this.api}/api/anime/list`, { + params: { + search: search, + type: type, + year: year, + genre: genre, + page: page, + }, + }); + + const animeSearchParseObj = data; + + const animeSearch: ResultSearch = { + nav: { + count: animeSearchParseObj.data.length, + current: animeSearchParseObj.current_page, + next: + animeSearchParseObj.data.length < 28 + ? 0 + : animeSearchParseObj.current_page + 1, + hasNext: animeSearchParseObj.data.length < 28 ? false : true, + }, + results: [], + }; + animeSearchParseObj.data.map((e) => { + const animeSearchData: AnimeSearch = { + name: e.name, + image: + "https://www.themoviedb.org/t/p/original" + + e.poster + + "?&w=53&q=95", + url: `/anime/animelatinohd/name/${e.slug}`, + type: "", + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.log(error); } - + } } - - diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 31ee53d3..87bd3914 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -14,8 +14,9 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; +import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; -export class AnimeFlv { +export class AnimeFlv extends AnimeProviderModel { readonly url = "https://animeflv.ws"; async GetAnimeInfo(anime: string): Promise { @@ -75,7 +76,7 @@ export class AnimeFlv { } } - async Filter( + async GetAnimeByFilter( gen?: Genres | string, date?: string, type?: TypeAnimeflv, @@ -128,22 +129,24 @@ export class AnimeFlv { const $ = load(data); const title = $(".CapiTop").children("h1").text().trim(); const getLinks = $(".CpCnA .anime_muti_link li"); - const numberEpisode = episode.substring(episode.lastIndexOf("-") + 1) + const numberEpisode = episode.substring(episode.lastIndexOf("-") + 1); const episodeReturn = new Episode(); episodeReturn.name = title; episodeReturn.url = `/anime/flv/episode/${episode}`; episodeReturn.number = numberEpisode as unknown as string; episodeReturn.servers = []; - const promises = getLinks.map(async(_i, e) => { + const promises = getLinks.map(async (_i, e) => { const servers = new EpisodeServer(); const title = $(e).attr("title"); const videoData = $(e).attr("data-video"); servers.name = title; servers.url = videoData; - if(videoData.includes("streaming.php")){ - await this.getM3U(`${videoData.replace("streaming.php", "ajax.php")}&refer=none`).then((g) => { - if(g.source.length){ + if (videoData.includes("streaming.php")) { + await this.getM3U( + `${videoData.replace("streaming.php", "ajax.php")}&refer=none` + ).then((g) => { + if (g.source.length) { servers.file_url = g.source[0].file; } }); @@ -185,7 +188,7 @@ export class AnimeFlv { return res.data; } catch (error) { - console.log(error) + console.log(error); } } } diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index fc26a693..f08aab55 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -2,104 +2,127 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "@animetypes/anime"; import { Episode, EpisodeServer } from "@animetypes/episode"; -import { AnimeSearch, ResultSearch, IResultSearch, IAnimeSearch } from "@animetypes/search"; - -export class AnimeLatinoHD { - readonly url = "https://www.animelatinohd.com"; - readonly api = "https://api.animelatinohd.com"; - - async GetAnimeInfo(anime: string): Promise { - try { - const { data } = await axios.get(`${this.url}/anime/${anime}`); - const $ = cheerio.load(data); - - const animeInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data - const Dates = new Date(String(animeInfoParseObj.aired)) - const DateFormat = new Intl.DateTimeFormat("en",{day:"numeric",month:"numeric",year:"numeric"}).format(Dates).split("/") - - const AnimeInfo: Anime = { - name: animeInfoParseObj.name, - url: `/anime/animelatinohd/name/${anime}`, - synopsis: animeInfoParseObj.overview, - alt_name: [...animeInfoParseObj.name_alternative.split(",")], - image: { - url: "https://www.themoviedb.org/t/p/original" + animeInfoParseObj.poster + "?&w=53&q=95" - }, - genres: [...animeInfoParseObj.genres.split(",")], - type: animeInfoParseObj.type, - status: animeInfoParseObj.status == 1 ? "En emisión" : "Finalizado", - date: {year:DateFormat[2],month:DateFormat[1],day:DateFormat[0]}, - episodes: [] - } - - animeInfoParseObj.episodes.map(e => { - const AnimeEpisode: Episode = { - name: animeInfoParseObj.name, - number: e.number + "", - image: "https://www.themoviedb.org/t/p/original" + animeInfoParseObj.banner + "?&w=280&q=95", - url: `/anime/animelatinohd/episode/${animeInfoParseObj.slug + "-" + e.number}` - } - - AnimeInfo.episodes.push(AnimeEpisode); - }) - - return AnimeInfo; - - } catch (error) { - console.log(error) - } +import { + AnimeSearch, + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "@animetypes/search"; +import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; + +export class AnimeLatinoHD extends AnimeProviderModel { + readonly url = "https://www.animelatinohd.com"; + readonly api = "https://api.animelatinohd.com"; + + async GetAnimeInfo(anime: string): Promise { + try { + const { data } = await axios.get(`${this.url}/anime/${anime}`); + const $ = cheerio.load(data); + + const animeInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props + .pageProps.data; + const Dates = new Date(String(animeInfoParseObj.aired)); + const DateFormat = new Intl.DateTimeFormat("en", { + day: "numeric", + month: "numeric", + year: "numeric", + }) + .format(Dates) + .split("/"); + + const AnimeInfo: Anime = { + name: animeInfoParseObj.name, + url: `/anime/animelatinohd/name/${anime}`, + synopsis: animeInfoParseObj.overview, + alt_name: [...animeInfoParseObj.name_alternative.split(",")], + image: { + url: + "https://www.themoviedb.org/t/p/original" + + animeInfoParseObj.poster + + "?&w=53&q=95", + }, + genres: [...animeInfoParseObj.genres.split(",")], + type: animeInfoParseObj.type, + status: animeInfoParseObj.status == 1 ? "En emisión" : "Finalizado", + date: { year: DateFormat[2], month: DateFormat[1], day: DateFormat[0] }, + episodes: [], + }; + + animeInfoParseObj.episodes.map((e) => { + const AnimeEpisode: Episode = { + name: animeInfoParseObj.name, + number: e.number + "", + image: + "https://www.themoviedb.org/t/p/original" + + animeInfoParseObj.banner + + "?&w=280&q=95", + url: `/anime/animelatinohd/episode/${ + animeInfoParseObj.slug + "-" + e.number + }`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.log(error); } - async GetEpisodeServers(episode: string, lang: string): Promise { - try { - - const number = episode.substring(episode.lastIndexOf("-") + 1) - const anime = episode.substring(0, episode.lastIndexOf("-")) - const langType = [{ lang: "es", type: "Latino" }, { lang: "jp", type: "Subtitulado" }] - - const { data } = await axios.get(`${this.url}/ver/${anime}/${number}`); - const $ = cheerio.load(data); - - const animeEpisodeParseObj = JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data - - const AnimeEpisodeInfo: Episode = { - name: animeEpisodeParseObj.anime.name, - url: `/anime/animelatinohd/episode/${episode}`, - number: number, - image: "", - servers: [] - } - - const sel_lang = langType.filter((e) => e.lang == lang) - let f_index = 0 - - if (sel_lang.length) { - $("#languaje option").each((_i, e) => { - if ($(e).text() == sel_lang[0].type) { - f_index = Number($(e).val()) - } - }) - } else { - $("#languaje option").each((_i, e) => { - f_index = Number($(e).val()) - }) - } - - await Promise.all(animeEpisodeParseObj.players[f_index].map(async (e: { server: { title: string; }; id: string; }) => { - //const min = await axios.get("https://filemoon.sx/e/smone1s7jjxv/CYM01HNMCGTSKT") - //const pageload = await BrowserHandler("https://animelatinohd.com/") - - const Server: EpisodeServer = { - name: e.server.title, - url: "", - } - //const cookies = [{name: 'v_id', value: "https://api.animelatinohd.com/stream/"+e.id},]; - Server.url = "https://api.animelatinohd.com/stream/" + e.id - Server.name = e.server.title - - - - //await pageload.page.setCookie(...cookies) - /*await pageload.page.evaluate(()=>{ + } + async GetEpisodeServers(episode: string, lang: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + const langType = [ + { lang: "es", type: "Latino" }, + { lang: "jp", type: "Subtitulado" }, + ]; + + const { data } = await axios.get(`${this.url}/ver/${anime}/${number}`); + const $ = cheerio.load(data); + + const animeEpisodeParseObj = JSON.parse($("#__NEXT_DATA__").html()).props + .pageProps.data; + + const AnimeEpisodeInfo: Episode = { + name: animeEpisodeParseObj.anime.name, + url: `/anime/animelatinohd/episode/${episode}`, + number: number, + image: "", + servers: [], + }; + + const sel_lang = langType.filter((e) => e.lang == lang); + let f_index = 0; + + if (sel_lang.length) { + $("#languaje option").each((_i, e) => { + if ($(e).text() == sel_lang[0].type) { + f_index = Number($(e).val()); + } + }); + } else { + $("#languaje option").each((_i, e) => { + f_index = Number($(e).val()); + }); + } + + await Promise.all( + animeEpisodeParseObj.players[f_index].map( + async (e: { server: { title: string }; id: string }) => { + //const min = await axios.get("https://filemoon.sx/e/smone1s7jjxv/CYM01HNMCGTSKT") + //const pageload = await BrowserHandler("https://animelatinohd.com/") + + const Server: EpisodeServer = { + name: e.server.title, + url: "", + }; + //const cookies = [{name: 'v_id', value: "https://api.animelatinohd.com/stream/"+e.id},]; + Server.url = "https://api.animelatinohd.com/stream/" + e.id; + Server.name = e.server.title; + + //await pageload.page.setCookie(...cookies) + /*await pageload.page.evaluate(()=>{ function getCookie(cname) { const name = cname + "="; const decodedCookie = decodeURIComponent(document.cookie); @@ -129,8 +152,8 @@ export class AnimeLatinoHD { Server.url = unp*/ - //state 1 - /*if (e.server.title == "Beta") { + //state 1 + /*if (e.server.title == "Beta") { let sel = dat("script:contains('var foo_ui = function (event) {')") let sort = String(sel.html()) let domain = eval(sort.slice(sort.search("const url"), sort.search("const langDef")).replace("const url =", "").trim()) @@ -150,53 +173,64 @@ export class AnimeLatinoHD { Server.url = "https://filemoon.sx" + "/e/" + id_file }*/ - AnimeEpisodeInfo.servers.push(Server) - })) + AnimeEpisodeInfo.servers.push(Server); + } + ) + ); - return AnimeEpisodeInfo; - } catch (error) { - console.log(error) - } + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); } - - async GetAnimeByFilter(search?: string, type?: number, page?: number, year?: string, genre?: string): Promise> { - try { - const { data } = await axios.get(`${this.api}/api/anime/list`, { - params: { - search: search, - type: type, - year: year, - genre: genre, - page: page - } - }); - - const animeSearchParseObj = data - - const animeSearch: ResultSearch = { - nav: { - count: animeSearchParseObj.data.length, - current: animeSearchParseObj.current_page, - next: animeSearchParseObj.data.length < 28 ? 0 : animeSearchParseObj.current_page + 1, - hasNext: animeSearchParseObj.data.length < 28 ? false : true - }, - results: [] - } - animeSearchParseObj.data.map(e => { - const animeSearchData: AnimeSearch = { - name: e.name, - image: "https://www.themoviedb.org/t/p/original" + e.poster + "?&w=53&q=95", - url: `/anime/animelatinohd/name/${e.slug}`, - type: "" - } - animeSearch.results.push(animeSearchData) - }) - return animeSearch; - } catch (error) { - console.log(error) - } + } + + async GetAnimeByFilter( + search?: string, + type?: number, + page?: number, + year?: string, + genre?: string + ): Promise> { + try { + const { data } = await axios.get(`${this.api}/api/anime/list`, { + params: { + search: search, + type: type, + year: year, + genre: genre, + page: page, + }, + }); + + const animeSearchParseObj = data; + + const animeSearch: ResultSearch = { + nav: { + count: animeSearchParseObj.data.length, + current: animeSearchParseObj.current_page, + next: + animeSearchParseObj.data.length < 28 + ? 0 + : animeSearchParseObj.current_page + 1, + hasNext: animeSearchParseObj.data.length < 28 ? false : true, + }, + results: [], + }; + animeSearchParseObj.data.map((e) => { + const animeSearchData: AnimeSearch = { + name: e.name, + image: + "https://www.themoviedb.org/t/p/original" + + e.poster + + "?&w=53&q=95", + url: `/anime/animelatinohd/name/${e.slug}`, + type: "", + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.log(error); } - + } } - - diff --git a/src/scraper/sites/anime/gogoanime/Gogoanime.ts b/src/scraper/sites/anime/gogoanime/Gogoanime.ts index 05b711a7..301d40da 100644 --- a/src/scraper/sites/anime/gogoanime/Gogoanime.ts +++ b/src/scraper/sites/anime/gogoanime/Gogoanime.ts @@ -1,4 +1,4 @@ -import { getHTML } from "./assets/getHTML"; +/* import { getHTML } from "./assets/getHTML"; import { Anime } from "../../../../types/anime"; import { getAllAnimes } from "./assets/getAllAnimesHTML"; import { Episode } from "../../../../types/episode"; @@ -178,3 +178,4 @@ export class GogoanimeServer { + */ diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index e9b1c033..c48c4890 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -2,182 +2,286 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "@animetypes/anime"; import { Episode, EpisodeServer } from "@animetypes/episode"; -import { IResultSearch, IAnimeSearch, ResultSearch, AnimeSearch } from "@animetypes/search"; -import { UnPacked } from"@animetypes/utils"; +import { + IResultSearch, + IAnimeSearch, + ResultSearch, + AnimeSearch, +} from "@animetypes/search"; +import { UnPacked } from "@animetypes/utils"; +import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; /** List of Domains * https://wcostream.tv - * + * * https://m.wcostream.org (phone) - * + * * https://wcopanel.cizgifilmlerizle.com * https://neptun.cizgifilmlerizle.com - * + * * https://ndisk[>1].cizgifilmlerizle.com * https://neptun[>1].cizgifilmlerizle.com - * + * * https://cdn.animationexplore.com * https://animationexplore.com - * + * * https://watchanimesub.net * https://lb.watchanimesub.net - * + * * https://www.wcopremium.tv -*/ + */ //Default Set Axios Cookie -axios.defaults.withCredentials = true -axios.defaults.headers.common["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"; - -export class WcoStream { - readonly url = "https://www.wcostream.tv"; - - async GetAnimeInfo(anime: string): Promise { - try { - const { data } = await axios.get(`${this.url}/anime/${anime}`, { headers: { "User-Agent": "Mozilla/5.0 (Linux; Android 10; LM-K920) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36" } }); - const $ = cheerio.load(data); - - const image = $("#category_description .ui-grid-solo .ui-block-a img").attr("src") - const name = $(".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x").text().replace("Share On", "") - const genre = $(".ui-grid-solo.left .ui-block-a").text().replace("Genre;", "").replace("Language; ", "") - - const AnimeInfo: Anime = { - name: name, - url: `/anime/wcostream/name/${anime}`, - synopsis: $("#category_description .ui-grid-solo .ui-block-a div p").text().replace("Watch ", "").replace(/\n/g, ""), - image: { - url: !image.includes("https://") ? image.replace("//", "https://") : image - }, - genres: [...genre.replace(genre.includes("Dubbed") ? "Dubbed" : "Subbed", "").trim().replace(/\n/g, "").replace(/\s+/g, "").replace("-", "").split(",").map(v => v.trim())], - episodes: [] - } - - $("ul.ui-listview-z li").map((_i, e) => { - const data = $(e).find("a").text() - const episode = data.slice(data.search(" Episode ")).replace(data.includes("English Dubbed") ? "English Dubbed" : "English Subbed", "").replace("Episode", "").trim().replace(/[^0-9-.]/g, "") - const season = data.includes("Season") ? data.slice(data.search(" Season "), data.search(" Episode ")).replace("Season", "").trim() : "" - - if (data && !data.includes("Movie") && !data.includes("OVA")) { - const AnimeEpisode: Episode = { - name: data, - number: episode, - image: `https://cdn.animationexplore.com/thumbs/${$(e).find("a").attr("href").replace("https://www.wcostream.tv/", "").replace("/", "").replace(/[^a-zA-Z0-9 ]/g, " ").replace(/\s+/g, "-")}.jpg`, - url: `/anime/wcostream/episode/${anime.replace(/[^a-zA-Z0-9 ]/g, ' ').replace(/\s+/g, "-") + "-" + episode}${season ? "?season=" + season : ""}` - } - - AnimeInfo.episodes.push(AnimeEpisode); - } - }) - - return AnimeInfo; - - } catch (error) { - console.log(error) - } - } +axios.defaults.withCredentials = true; +axios.defaults.headers.common["User-Agent"] = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"; + +export class WcoStream extends AnimeProviderModel { + readonly url = "https://www.wcostream.tv"; + + async GetAnimeInfo(anime: string): Promise { + try { + const { data } = await axios.get(`${this.url}/anime/${anime}`, { + headers: { + "User-Agent": + "Mozilla/5.0 (Linux; Android 10; LM-K920) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36", + }, + }); + const $ = cheerio.load(data); - // Global Apis https://www.wcostream.org/wp-json - // https://www.wcostream.org/wp-json/wp/v2/pages + const image = $( + "#category_description .ui-grid-solo .ui-block-a img" + ).attr("src"); + const name = $( + ".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x" + ) + .text() + .replace("Share On", ""); + const genre = $(".ui-grid-solo.left .ui-block-a") + .text() + .replace("Genre;", "") + .replace("Language; ", ""); - async GetEpisodeServers(episode: string, season: number | string) { - try { + const AnimeInfo: Anime = { + name: name, + url: `/anime/wcostream/name/${anime}`, + synopsis: $("#category_description .ui-grid-solo .ui-block-a div p") + .text() + .replace("Watch ", "") + .replace(/\n/g, ""), + image: { + url: !image.includes("https://") + ? image.replace("//", "https://") + : image, + }, + genres: [ + ...genre + .replace(genre.includes("Dubbed") ? "Dubbed" : "Subbed", "") + .trim() + .replace(/\n/g, "") + .replace(/\s+/g, "") + .replace("-", "") + .split(",") + .map((v) => v.trim()), + ], + episodes: [], + }; - const NumEpisode = episode.substring(episode.lastIndexOf("-") + 1) - const anime = episode.substring(0, episode.lastIndexOf("-")) + $("ul.ui-listview-z li").map((_i, e) => { + const data = $(e).find("a").text(); + const episode = data + .slice(data.search(" Episode ")) + .replace( + data.includes("English Dubbed") + ? "English Dubbed" + : "English Subbed", + "" + ) + .replace("Episode", "") + .trim() + .replace(/[^0-9-.]/g, ""); + const season = data.includes("Season") + ? data + .slice(data.search(" Season "), data.search(" Episode ")) + .replace("Season", "") + .trim() + : ""; - const { data } = await axios.get(`https://www.wcostream.tv/playlist-cat/${anime}`) - const $ = cheerio.load(data); + if (data && !data.includes("Movie") && !data.includes("OVA")) { + const AnimeEpisode: Episode = { + name: data, + number: episode, + image: `https://cdn.animationexplore.com/thumbs/${$(e) + .find("a") + .attr("href") + .replace("https://www.wcostream.tv/", "") + .replace("/", "") + .replace(/[^a-zA-Z0-9 ]/g, " ") + .replace(/\s+/g, "-")}.jpg`, + url: `/anime/wcostream/episode/${ + anime.replace(/[^a-zA-Z0-9 ]/g, " ").replace(/\s+/g, "-") + + "-" + + episode + }${season ? "?season=" + season : ""}`, + }; - const mainUrl = $("script").get()[3].children[0].data - const mainOrigin = eval(mainUrl.trim().slice(mainUrl.search("playlist:") + 6, mainUrl.search('image: ') - 4).trim().replace(",", "")) + AnimeInfo.episodes.push(AnimeEpisode); + } + }); - const mainData = await axios.get(this.url + mainOrigin) - const $$ = cheerio.load(mainData.data.replaceAll(":image", " type='image'").replaceAll(":source", " type='video'").trim()) + return AnimeInfo; + } catch (error) { + console.log(error); + } + } - const AnimeEpisodeInfo: Episode = { - name: "", - url: `/anime/wcostream/episode/${episode}${season ? "?season=" + season : ""}`, - number: NumEpisode, - image: "", - servers: [] - } + // Global Apis https://www.wcostream.org/wp-json + // https://www.wcostream.org/wp-json/wp/v2/pages + async GetEpisodeServers(episode: string, season: number | string) { + try { + const NumEpisode = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); - $$("item").each(async (_i, e) => { - const title = $$(e).find("title").text() + const { data } = await axios.get( + `https://www.wcostream.tv/playlist-cat/${anime}` + ); + const $ = cheerio.load(data); - if (title.includes("Episode " + NumEpisode + " ") && !season && !title.includes("Season")) { - AnimeEpisodeInfo.name = title.replace("", "").trim() - AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text() - const Server: EpisodeServer = { - name: "JWplayer - " + $$(e).find("jwplayer[type='video']").attr("label"), - url: $$(e).find("jwplayer[type='video']").attr("file"), - } - AnimeEpisodeInfo.servers.push(Server); + const mainUrl = $("script").get()[3].children[0].data; + const mainOrigin = eval( + mainUrl + .trim() + .slice(mainUrl.search("playlist:") + 6, mainUrl.search("image: ") - 4) + .trim() + .replace(",", "") + ); - } else if (title.includes("Episode " + NumEpisode + " ") && season && title.includes("Season " + season)) { - AnimeEpisodeInfo.name = title.replace("", "").trim() - AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text() + const mainData = await axios.get(this.url + mainOrigin); + const $$ = cheerio.load( + mainData.data + .replaceAll(":image", " type='image'") + .replaceAll(":source", " type='video'") + .trim() + ); - const Server: EpisodeServer = { - name: "JWplayer - " + $$(e).find("jwplayer[type='video']").attr("label"), - url: $$(e).find("jwplayer[type='video']").attr("file"), - } + const AnimeEpisodeInfo: Episode = { + name: "", + url: `/anime/wcostream/episode/${episode}${ + season ? "?season=" + season : "" + }`, + number: NumEpisode, + image: "", + servers: [], + }; - AnimeEpisodeInfo.servers.push(Server); - } - }) - return AnimeEpisodeInfo; - } catch (error) { - console.log(error) - } - } + $$("item").each(async (_i, e) => { + const title = $$(e).find("title").text(); - async GetAnimeByFilter(search?: string, page?: number): Promise> { - try { - const formdata = new FormData(); - formdata.append("catara", search); - formdata.append("konuara", "series"); - - const { data } = await axios.post(`${this.url}/search`, formdata); - - const $ = cheerio.load(data) - const animeSearch: ResultSearch = { - nav: { - count: $("#blog .cerceve").length, - current: Number(page ? page : 1), - next: $("#blog .cerceve").length < 28 ? 0 : page ? Number(page) + 1 : 2, - hasNext: $("#blog .cerceve").length < (28 * page) ? false : true - }, - results: [] - } - - $("#blog .cerceve").each((i, e) => { - if ((animeSearch.nav.current > 1 ? i - 1 : i) >= 28 * (animeSearch.nav.current - 1) && (animeSearch.nav.current > 1 ? i + 1 : i) <= 28 * animeSearch.nav.current) { - const animeSearchData: AnimeSearch = { - name: $(e).find(".iccerceve a").attr("title"), - image: $(e).find(".iccerceve a img").attr("src"), - url: `/anime/wcostream/name/${$(e).find(".iccerceve a").attr("href").replace("/anime/", "")}`, - type: "anime" - } - animeSearch.results.push(animeSearchData) - } - }) - return animeSearch; - } catch (error) { - console.log(error) + if ( + title.includes("Episode " + NumEpisode + " ") && + !season && + !title.includes("Season") + ) { + AnimeEpisodeInfo.name = title + .replace("", "") + .trim(); + AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text(); + const Server: EpisodeServer = { + name: + "JWplayer - " + + $$(e).find("jwplayer[type='video']").attr("label"), + url: $$(e).find("jwplayer[type='video']").attr("file"), + }; + AnimeEpisodeInfo.servers.push(Server); + } else if ( + title.includes("Episode " + NumEpisode + " ") && + season && + title.includes("Season " + season) + ) { + AnimeEpisodeInfo.name = title + .replace("", "") + .trim(); + AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text(); + + const Server: EpisodeServer = { + name: + "JWplayer - " + + $$(e).find("jwplayer[type='video']").attr("label"), + url: $$(e).find("jwplayer[type='video']").attr("file"), + }; + + AnimeEpisodeInfo.servers.push(Server); } + }); + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); } + } + + async GetAnimeByFilter( + search?: string, + page?: number + ): Promise> { + try { + const formdata = new FormData(); + formdata.append("catara", search); + formdata.append("konuara", "series"); + + const { data } = await axios.post(`${this.url}/search`, formdata); - async RuntimeUnpacked(data:string) { - const content = Buffer.from(data, 'base64').toString() - const $ = cheerio.load(content) - const Buffers = $("script").get().at(-1).children[0].data - const UnBuffer = UnPacked(Buffer.from(Buffers).toString('base64')) - const RequestBR = await eval(UnBuffer.slice(UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, UnBuffer.indexOf("}],image:", 1))); + const $ = cheerio.load(data); + const animeSearch: ResultSearch = { + nav: { + count: $("#blog .cerceve").length, + current: Number(page ? page : 1), + next: + $("#blog .cerceve").length < 28 ? 0 : page ? Number(page) + 1 : 2, + hasNext: $("#blog .cerceve").length < 28 * page ? false : true, + }, + results: [], + }; - return RequestBR + $("#blog .cerceve").each((i, e) => { + if ( + (animeSearch.nav.current > 1 ? i - 1 : i) >= + 28 * (animeSearch.nav.current - 1) && + (animeSearch.nav.current > 1 ? i + 1 : i) <= + 28 * animeSearch.nav.current + ) { + const animeSearchData: AnimeSearch = { + name: $(e).find(".iccerceve a").attr("title"), + image: $(e).find(".iccerceve a img").attr("src"), + url: `/anime/wcostream/name/${$(e) + .find(".iccerceve a") + .attr("href") + .replace("/anime/", "")}`, + type: "anime", + }; + animeSearch.results.push(animeSearchData); + } + }); + return animeSearch; + } catch (error) { + console.log(error); } -} + } + async RuntimeUnpacked(data: string) { + const content = Buffer.from(data, "base64").toString(); + const $ = cheerio.load(content); + const Buffers = $("script").get().at(-1).children[0].data; + const UnBuffer = UnPacked(Buffer.from(Buffers).toString("base64")); + const RequestBR = await eval( + UnBuffer.slice( + UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, + UnBuffer.indexOf("}],image:", 1) + ) + ); + return RequestBR; + } +} diff --git a/src/scraper/sites/anime/zoro/Zoro.ts b/src/scraper/sites/anime/zoro/Zoro.ts index fed0d91c..1d47cdef 100644 --- a/src/scraper/sites/anime/zoro/Zoro.ts +++ b/src/scraper/sites/anime/zoro/Zoro.ts @@ -2,9 +2,14 @@ import axios from "axios"; import { load } from "cheerio"; import { Anime, Chronology } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; -import { AnimeSearch, ResultSearch, IAnimeSearch } from "../../../../types/search"; +import { + AnimeSearch, + ResultSearch, + IAnimeSearch, +} from "../../../../types/search"; +import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; -export class Zoro { +export class Zoro extends AnimeProviderModel { readonly url = "https://aniwatch.to"; async GetAnimeInfo(animeName: string): Promise { @@ -58,7 +63,7 @@ export class Zoro { } } //filter - async Filter( + async GetAnimeByFilter( type?: string, rated?: string, score?: string, @@ -66,10 +71,9 @@ export class Zoro { language?: string, sort?: string, genres?: string, - page_anime?: string, + page_anime?: string ) { try { - const { data } = await axios.get(`${this.url}/filter`, { params: { type: type, @@ -82,7 +86,7 @@ export class Zoro { page: page_anime || 1, }, }); - + const $ = load(data); const most_cards = $("div.film_list div.film_list-wrap div.flw-item"); //const page_index = $("div.pre-pagination nav ul li.active"); @@ -110,7 +114,7 @@ export class Zoro { } //episode server - async GetEpisodeServer(episode: string, ep: string) { + async GetEpisodeServers(episode: string, ep: string) { try { const animename = episode.toLowerCase().replace(/\s/g, "-"); const { data } = await axios.get( From 4a4994a373837e516c27806b54a1ca18b69589b4 Mon Sep 17 00:00:00 2001 From: koikiss-dev Date: Sat, 2 Mar 2024 16:37:18 -0600 Subject: [PATCH 02/64] add a base provider and format (automatic) --- .eslintrc.json | 34 +- jest.config.js | 8 +- puppeteer.config.js | 4 +- src/index.ts | 20 +- src/routes/providers.ts | 2 +- src/routes/v1/anime/9anime/9animeRoute.js | 2 +- .../v1/anime/animeblix/AnimeBlixRoutes.ts | 33 +- .../v1/anime/animeflv/AnimeflvRoutes.ts | 2 +- .../animelatinohd/AnimeLatinoHDRoutes.ts | 33 +- .../v1/anime/animevostfr/AnimevostfrRoutes.ts | 31 +- .../v1/anime/monoschinos/MonosChinosRoute.ts | 6 +- src/routes/v1/anime/otakutv/otakutvRoute.js | 62 +-- src/routes/v1/anime/tioanime/TioAnimeRoute.ts | 32 +- .../v1/anime/wcostream/wcostreamRoutes.ts | 44 +- src/routes/v1/anime/zoro/ZoroRoutes.ts | 13 +- .../v1/doramas/dramanice/DramaniceRoutes.ts | 33 +- src/routes/v1/manga/comick/ComickRoutes.ts | 31 +- src/routes/v1/manga/inmanga/InmangaRoutes.ts | 33 +- .../v1/manga/manganelo/ManganeloRoutes.ts | 12 +- .../v1/manga/mangareader/MangaReaderRoutes.ts | 19 +- src/scraper/sites/anime/9Anime/9Anime.js | 2 +- .../sites/anime/animeBlixs/AnimeBlix.ts | 13 +- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 12 +- .../sites/anime/animeflv/animeflv_helper.ts | 94 ++-- .../anime/animelatinohd/AnimeLatinoHD.ts | 7 +- .../sites/anime/animevostfr/Animevostfr.ts | 333 ++++++------ .../gogoanime/assets/getAllAnimesHTML.ts | 96 ++-- .../sites/anime/gogoanime/assets/getHTML.ts | 7 - .../sites/anime/monoschinos/Monoschinos.ts | 300 ++++++----- src/scraper/sites/anime/otakuTV/getAnime.js | 20 +- .../sites/anime/otakuTV/getAnimeComingSoon.js | 2 +- .../sites/anime/otakuTV/getAnimeInfo.js | 19 +- .../sites/anime/otakuTV/getAnimeNew.js | 2 +- .../sites/anime/otakuTV/getAnimePremiere.js | 2 +- .../sites/anime/otakuTV/getAnimeRanking.js | 7 +- .../sites/anime/otakuTV/getAnimeServer.js | 27 +- .../sites/anime/otakuTV/getUsersActive.js | 2 +- src/scraper/sites/anime/otakuTV/search.js | 40 +- src/scraper/sites/anime/tioanime/TioAnime.ts | 472 ++++++++++-------- .../sites/anime/wcostream/WcoStream.ts | 19 +- src/scraper/sites/anime/zoro/Zoro.ts | 8 +- .../sites/doramas/dramanice/Dramanice.ts | 303 ++++++----- .../sites/manga/MangaBuddy/MangaBuddy.ts | 4 +- .../sites/manga/MangaReader/MangaReader.ts | 177 ++++--- .../manga/MangaReader/MangaReaderTypes.ts | 21 +- src/scraper/sites/manga/comick/Comick.ts | 397 ++++++++------- src/scraper/sites/manga/inmanga/Inmanga.ts | 392 +++++++++------ .../sites/manga/manganelo/Manganelo.ts | 158 +++--- src/scraper/sites/manga/nhentai/Nhentai.ts | 3 +- src/scraper/sites/manga/tmomanga/Page.js | 143 +++--- src/scraper/sites/manga/tmomanga/filter.js | 55 +- src/scraper/sites/news/kudasai/kudasai.js | 100 ++-- src/test/Animeflv.spec.ts | 62 ++- src/test/Animelatinohd.spec.ts | 2 +- src/test/Comick.spec.ts | 5 +- src/test/Inmanga.spec.ts | 5 +- src/test/MangaReader.spec.ts | 264 +++++----- src/test/Manganelo.spec.ts | 85 ++-- src/test/TioAnime.spec.ts | 16 +- src/test/Zoro.spec.ts | 38 +- src/types/anime.ts | 308 ++++++------ src/types/date.ts | 184 +++---- src/types/episode.ts | 168 +++---- src/types/extractors.ts | 57 +-- src/types/filter.ts | 82 +-- src/types/image.ts | 72 +-- src/types/index.ts | 8 +- src/types/manga.ts | 4 +- src/types/movie.d.ts | 39 +- src/types/search.ts | 174 +++---- src/types/utils.ts | 97 ++-- src/utils/manga/schemaProviders.js | 154 +++--- src/utils/schemaProviders.js | 30 +- src/utils/shemaNewsProviders.js | 72 +-- src/utils/shemaProvidersExperimental.js | 174 +++---- src/utils/utilities.js | 10 +- tsconfig.json | 25 +- vercel.json | 2 +- 78 files changed, 3117 insertions(+), 2711 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d0c38f8d..5e3ef36b 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,22 +1,16 @@ { - "env": { - "browser": true, - "es2021": true - }, - "parser": "@typescript-eslint/parser", - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended" - ], - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module", - "project": ["./tsconfig.json"] - }, - "plugins": [ - "@typescript-eslint" - ], - "root": true, - "rules": { - } + "env": { + "browser": true, + "es2021": true + }, + "parser": "@typescript-eslint/parser", + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": ["./tsconfig.json"] + }, + "plugins": ["@typescript-eslint"], + "root": true, + "rules": {} } diff --git a/jest.config.js b/jest.config.js index 260c66e6..f590a7c3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,8 @@ module.exports = { - roots: [ - "/src", - ], + roots: ["/src"], testMatch: [ "**/__tests__/**/*.+(ts|tsx|js)", - "**/?(*.)+(spec|test).+(ts|tsx|js)" + "**/?(*.)+(spec|test).+(ts|tsx|js)", ], transform: { "^.+\\.(ts|tsx)$": [ @@ -19,4 +17,4 @@ module.exports = { "!**/*.d.ts", "!**/node_modules/**", ], -} +}; diff --git a/puppeteer.config.js b/puppeteer.config.js index 3996fd8b..bf95408c 100644 --- a/puppeteer.config.js +++ b/puppeteer.config.js @@ -4,5 +4,5 @@ const { Configuration } = require("puppeteer"); /** @type {Configuration} */ module.exports = { - cacheDirectory: join(os.homedir(), ".cache", "puppeteer") -} + cacheDirectory: join(os.homedir(), ".cache", "puppeteer"), +}; diff --git a/src/index.ts b/src/index.ts index 2992c175..7b443354 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,15 +3,15 @@ import morgan from "morgan"; import index from "../src/routes/app"; import providersList from "../src/routes/providers"; import helmet from "helmet"; -import cors from 'cors' +import cors from "cors"; /* Anime */ import flv from "../src/routes/v1/anime/animeflv/AnimeflvRoutes"; import latinhd from "../src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes"; -import gogoanime from "../src/routes/v1/anime/gogoanime/GogoAnimeRoute"; +//import gogoanime from "../src/routes/v1/anime/gogoanime/GogoAnimeRoute"; import zoro from "../src/routes/v1/anime/zoro/ZoroRoutes"; import monoschinos from "../src/routes/v1/anime/monoschinos/MonosChinosRoute"; -import tioanime from '../src/routes/v1/anime/tioanime/TioAnimeRoute' +import tioanime from "../src/routes/v1/anime/tioanime/TioAnimeRoute"; import WcoStream from "../src/routes/v1/anime/wcostream/wcostreamRoutes"; import AnimeBlix from "../src/routes/v1/anime/animeblix/AnimeBlixRoutes"; import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; @@ -19,7 +19,7 @@ import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; /* Manga */ import comick from "../src/routes/v1/manga/comick/ComickRoutes"; import inmanga from "../src/routes/v1/manga/inmanga/InmangaRoutes"; -import nhentai from "../src/routes/v1/manga/nhentai/NhentaiRoutes" +import nhentai from "../src/routes/v1/manga/nhentai/NhentaiRoutes"; import mangareader from "../src/routes/v1/manga/mangareader/MangaReaderRoutes"; import manganelo from "../src/routes/v1/manga/manganelo/ManganeloRoutes"; @@ -33,34 +33,31 @@ app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(morgan("dev")); app.use(helmet()); -app.use(cors()) +app.use(cors()); //routes /*anime*/ app.use(flv); app.use(latinhd); -app.use(gogoanime); +//app.use(gogoanime); app.use(monoschinos); app.use(zoro); -app.use(tioanime) +app.use(tioanime); app.use(WcoStream); app.use(AnimeBlix); app.use(Animevostfr); /* anime */ - /*Manga*/ app.use(comick); app.use(inmanga); -app.use(nhentai) +app.use(nhentai); app.use(mangareader); app.use(manganelo); /*Manga*/ - - /*error */ interface ErrorResponse { @@ -102,7 +99,6 @@ app.use((err, res, _next) => { res.status(response.error.status).send(response); }); - app.listen(port, () => { console.log(`Servidor iniciado en el puerto ${port} listo para trabajar :)`); }); diff --git a/src/routes/providers.ts b/src/routes/providers.ts index 610416c2..be419ce5 100644 --- a/src/routes/providers.ts +++ b/src/routes/providers.ts @@ -11,7 +11,7 @@ interface ProviderScraper { status: number | string; icon: string; url: string; - apiID: string + apiID: string; favicon: string | string[]; } diff --git a/src/routes/v1/anime/9anime/9animeRoute.js b/src/routes/v1/anime/9anime/9animeRoute.js index ab752711..91dded82 100644 --- a/src/routes/v1/anime/9anime/9animeRoute.js +++ b/src/routes/v1/anime/9anime/9animeRoute.js @@ -10,4 +10,4 @@ r.get("/anime/9anime/name/:name", (req, res) => { }); }); -export default r +export default r; diff --git a/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts b/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts index ed6dcb59..3e29104d 100644 --- a/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts +++ b/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts @@ -6,27 +6,32 @@ const router = Router(); // Filter router.get("/anime/animeblix/filter", async (req, res) => { - const { search, type, page, year, genre } = req.query - - const data = await Anime.GetAnimeByFilter(search as string, type as unknown as number, page as unknown as number, year as string, genre as string) - res.send(data) + const { search, type, page, year, genre } = req.query; + + const data = await Anime.GetAnimeByFilter( + search as string, + type as unknown as number, + page as unknown as number, + year as string, + genre as string, + ); + res.send(data); }); // Anime Info +(Episodes list) router.get("/anime/animeblix/name/:name", async (req, res) => { - - const { name } = req.params - const data = await Anime.GetAnimeInfo(name.includes("ver-")? name.replace("ver-","") : name) - res.send(data) - + const { name } = req.params; + const data = await Anime.GetAnimeInfo( + name.includes("ver-") ? name.replace("ver-", "") : name, + ); + res.send(data); }); // Episode Info +(Video Servers) router.get("/anime/animeblix/episode/:episode", async (req, res) => { - const { episode } = req.params - const data = await Anime.GetEpisodeServers(episode) - res.send(data) - + const { episode } = req.params; + const data = await Anime.GetEpisodeServers(episode); + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts index 3a5bad96..d230be2d 100644 --- a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts +++ b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts @@ -53,7 +53,7 @@ r.get("/anime/flv/filter", async (req, res) => { status, ord, page, - title + title, ); res.send(animeInfo); } catch (error) { diff --git a/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts b/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts index 9632b116..d9a380cc 100644 --- a/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts +++ b/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts @@ -5,28 +5,31 @@ const router = Router(); // Filter router.get("/anime/animelatinohd/filter", async (req, res) => { - const { search, type, page, year, genre } = req.query - - const data = await Anime.GetAnimeByFilter(search as string, type as unknown as number, page as unknown as number, year as string, genre as string) - res.send(data) + const { search, type, page, year, genre } = req.query; + + const data = await Anime.GetAnimeByFilter( + search as string, + type as unknown as number, + page as unknown as number, + year as string, + genre as string, + ); + res.send(data); }); // Anime Info +(Episodes list) router.get("/anime/animelatinohd/name/:name", async (req, res) => { - - const { name } = req.params - const data = await Anime.GetAnimeInfo(name) - res.send(data) - + const { name } = req.params; + const data = await Anime.GetAnimeInfo(name); + res.send(data); }); // Episode Info +(Video Servers) router.get("/anime/animelatinohd/episode/:episode", async (req, res) => { - const { lang } = req.query - const { episode } = req.params - const data = await Anime.GetEpisodeServers(episode, lang as string) - res.send(data) - + const { lang } = req.query; + const { episode } = req.params; + const data = await Anime.GetEpisodeServers(episode, lang as string); + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts index 3bdd76f2..a786c5d9 100644 --- a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts +++ b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts @@ -5,27 +5,30 @@ const router = Router(); // Filter router.get("/anime/animevostfr/filter", async (req, res) => { - const { search, type, page, year, genre } = req.query - - const data = await Anime.GetAnimeByFilter(search as string, type as unknown as number, page as unknown as number, year as string, genre as string) - res.send(data) + const { search, type, page, year, genre } = req.query; + + const data = await Anime.GetAnimeByFilter( + search as string, + type as unknown as number, + page as unknown as number, + year as string, + genre as string, + ); + res.send(data); }); // Anime Info +(Episodes list) router.get("/anime/animevostfr/name/:name", async (req, res) => { - - const { name } = req.params - const data = await Anime.GetAnimeInfo(name) - res.send(data) - + const { name } = req.params; + const data = await Anime.GetAnimeInfo(name); + res.send(data); }); // Episode Info +(Video Servers) router.get("/anime/animevostfr/episode/:episode", async (req, res) => { - const { episode } = req.params - const data = await Anime.GetEpisodeServers(episode) - res.send(data) - + const { episode } = req.params; + const data = await Anime.GetEpisodeServers(episode); + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts index eb10e7fb..23748f40 100644 --- a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts +++ b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts @@ -9,7 +9,7 @@ r.get("/anime/monoschinos/name/:name", async (req, res) => { const { name } = req.params; const monos = new Monoschinos(); const animeInfo = await monos.getAnime( - `https://monoschinos2.com/anime/${name}` + `https://monoschinos2.com/anime/${name}`, ); res.send(animeInfo); } catch (error) { @@ -24,7 +24,7 @@ r.get("/anime/monoschinos/episode/:episode", async (req, res) => { const { episode } = req.params; const monos = new Monoschinos(); const animeInfo = await monos.getEpisodeServers( - `https://monoschinos2.com/ver/${episode}` + `https://monoschinos2.com/ver/${episode}`, ); res.send(animeInfo); } catch (error) { @@ -36,7 +36,7 @@ r.get("/anime/monoschinos/episode/:episode", async (req, res) => { //filter r.get("/anime/monoschinos/filter", async (req, res) => { try { - const title = req.query.title as string + const title = req.query.title as string; const cat = req.query.category as string; const gen = req.query.gen as string; const year = req.query.year as string; diff --git a/src/routes/v1/anime/otakutv/otakutvRoute.js b/src/routes/v1/anime/otakutv/otakutvRoute.js index 106aae33..8914dfa7 100644 --- a/src/routes/v1/anime/otakutv/otakutvRoute.js +++ b/src/routes/v1/anime/otakutv/otakutvRoute.js @@ -1,53 +1,53 @@ import { Router } from "express"; import g from "../../../../scraper/sites/anime/otakuTV/getAnime.js"; import c from "../../../../scraper/sites/anime/otakuTV/getAnimeComingSoon.js"; -import l from '../../../../scraper/sites/anime/otakuTV/getAnimeLatino.js' +import l from "../../../../scraper/sites/anime/otakuTV/getAnimeLatino.js"; import n from "../../../../scraper/sites/anime/otakuTV/getAnimeNew.js"; -import ra from '../../../../scraper/sites/anime/otakuTV/getAnimeRanking.js' +import ra from "../../../../scraper/sites/anime/otakuTV/getAnimeRanking.js"; import u from "../../../../scraper/sites/anime/otakuTV/getUsersActive.js"; const r = Router(); //coming soon -r.get("/anime/otakuTV/comingsoon", (req, res)=>{ - c.getComingSoon().then(f =>{ - res.send(f) - }) -}) +r.get("/anime/otakuTV/comingsoon", (req, res) => { + c.getComingSoon().then((f) => { + res.send(f); + }); +}); //latino anime r.get("/anime/otakuTV/animelatin", (req, res) => { - l.getAnimeLatino().then(f => { - res.send(f) - }) -}) + l.getAnimeLatino().then((f) => { + res.send(f); + }); +}); //news r.get("/anime/otakuTV/animenew", (req, res) => { - n.getAnimeNew().then(f => { - res.send(f) - }) -}) + n.getAnimeNew().then((f) => { + res.send(f); + }); +}); -//anime ranking +//anime ranking r.get("/anime/otakuTV/animeranking", (req, res) => { - ra.getAnimeRanking().then(f => { - res.send(f) - }) -}) + ra.getAnimeRanking().then((f) => { + res.send(f); + }); +}); //user-ranking r.get("/anime/otakuTV/usertop", (req, res) => { - u.getUsersActive().then(f => { - res.send(f) - }) -}) + u.getUsersActive().then((f) => { + res.send(f); + }); +}); //name anime -r.get("/anime/otakuTV/:name", (req, res)=>{ - const {name} = req.params - g.getAnime(name).then(f => { - res.send(f) - }) -}) +r.get("/anime/otakuTV/:name", (req, res) => { + const { name } = req.params; + g.getAnime(name).then((f) => { + res.send(f); + }); +}); -export default r +export default r; diff --git a/src/routes/v1/anime/tioanime/TioAnimeRoute.ts b/src/routes/v1/anime/tioanime/TioAnimeRoute.ts index e4fbfcbe..46a170bb 100644 --- a/src/routes/v1/anime/tioanime/TioAnimeRoute.ts +++ b/src/routes/v1/anime/tioanime/TioAnimeRoute.ts @@ -8,7 +8,7 @@ r.get("/anime/tioanime/name/:name", async (req, res) => { const { name } = req.params; const tioanime = new TioAnime(); const animeInfo = await tioanime.getAnime( - `https://tioanime.com/anime/${name}` + `https://tioanime.com/anime/${name}`, ); res.send(animeInfo); } catch (error) { @@ -23,7 +23,7 @@ r.get("/anime/tioanime/episode/:episode", async (req, res) => { const { episode } = req.params; const tioanime = new TioAnime(); const animeInfo = await tioanime.getEpisodeServers( - `https://tioanime.com/ver/${episode}` + `https://tioanime.com/ver/${episode}`, ); res.send(animeInfo); } catch (error) { @@ -38,18 +38,18 @@ r.get("/anime/tioanime/last/:option", async (req, res) => { const { option } = req.params; const tioanime = new TioAnime(); - if ('episodes' === option) { + if ("episodes" === option) { res.send(await tioanime.getLastEpisodes()); - } else if ('animes' === option) { + } else if ("animes" === option) { res.send(await tioanime.getLastAnimes(null)); - } else if ('movies' === option) { + } else if ("movies" === option) { res.send(await tioanime.getLastMovies()); - } else if ('ovas' === option) { + } else if ("ovas" === option) { res.send(await tioanime.getLastOvas()); - } else if ('onas' === option) { + } else if ("onas" === option) { res.send(await tioanime.getLastOnas()); } else { - throw 'Invalid option in the URL'; + throw "Invalid option in the URL"; } } catch (error) { console.log(error); @@ -59,21 +59,29 @@ r.get("/anime/tioanime/last/:option", async (req, res) => { //filter r.get("/anime/tioanime/filter", async (req, res) => { - try { + try { const title = req.query.title as string; const types = (req.query.type as string[]) ?? []; const genres = (req.query.gen as string[]) ?? []; const begin = (req.query.begin_year as unknown as number) ?? 1950; - const end = (req.query.end_year as unknown as number) ?? new Date().getFullYear(); + const end = + (req.query.end_year as unknown as number) ?? new Date().getFullYear(); const status = (req.query.status as unknown as number) ?? 2; const sort = (req.query.sort as string) ?? "recent"; const tioanime = new TioAnime(); - const animeInfo = await tioanime.filter(title, types, genres, {begin, end}, status, sort); + const animeInfo = await tioanime.filter( + title, + types, + genres, + { begin, end }, + status, + sort, + ); res.send(animeInfo); - //console.log(tioanime.filter(types, genres, { begin: begin, end: end }, status, sort).then(result => { console.log(result) } )); + //console.log(tioanime.filter(types, genres, { begin: begin, end: end }, status, sort).then(result => { console.log(result) } )); } catch (error) { console.log(error); res.status(500).send(error); diff --git a/src/routes/v1/anime/wcostream/wcostreamRoutes.ts b/src/routes/v1/anime/wcostream/wcostreamRoutes.ts index 63a8bf4a..f6d0a56d 100644 --- a/src/routes/v1/anime/wcostream/wcostreamRoutes.ts +++ b/src/routes/v1/anime/wcostream/wcostreamRoutes.ts @@ -5,35 +5,41 @@ const Anime = new WcoStream(); const router = Router(); router.get("/anime/wcostream/name/:name", async (req, res) => { - const { name } = req.params - const data = await Anime.GetAnimeInfo(name) + const { name } = req.params; + const data = await Anime.GetAnimeInfo(name); - res.send(data) -}) + res.send(data); +}); router.get("/anime/wcostream/episode/:episode", async (req, res) => { - const { episode } = req.params - const { season } = req.query - const data = await Anime.GetEpisodeServers(episode, season as unknown as number) + const { episode } = req.params; + const { season } = req.query; + const data = await Anime.GetEpisodeServers( + episode, + season as unknown as number, + ); - res.send(data) -}) + res.send(data); +}); router.get("/anime/wcostream/filter", async (req, res) => { - const { search, page } = req.query - const data = await Anime.GetAnimeByFilter(search as string, page as unknown as number) + const { search, page } = req.query; + const data = await Anime.GetAnimeByFilter( + search as string, + page as unknown as number, + ); - res.send(data) -}) + res.send(data); +}); /* Global API */ -router.post("/runtime/unpacked", async (req,res) => { - const {base64} = req.body - const data = await Anime.RuntimeUnpacked(base64) - return res.send(data) -}) +router.post("/runtime/unpacked", async (req, res) => { + const { base64 } = req.body; + const data = await Anime.RuntimeUnpacked(base64); + return res.send(data); +}); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/anime/zoro/ZoroRoutes.ts b/src/routes/v1/anime/zoro/ZoroRoutes.ts index 931deede..d163b409 100644 --- a/src/routes/v1/anime/zoro/ZoroRoutes.ts +++ b/src/routes/v1/anime/zoro/ZoroRoutes.ts @@ -20,7 +20,7 @@ r.get("/anime/zoro/episode/:episode/:ep", async (req, res) => { try { const { episode, ep } = req.params; const zoro = new Zoro(); - const animeInfo = await zoro.GetEpisodeServer(episode, ep); + const animeInfo = await zoro.GetEpisodeServers(episode, ep); res.send(animeInfo); } catch (error) { console.log(error); @@ -41,7 +41,16 @@ r.get("/anime/zoro/filter", async (req, res) => { const page = req.query.page as string; const zoro = new Zoro(); - const animeInfo = await zoro.Filter(type, rated, score, season, language, sort, gen, page); + const animeInfo = await zoro.GetAnimeByFilter( + type, + rated, + score, + season, + language, + sort, + gen, + page, + ); res.send(animeInfo); } catch (error) { console.log(error); diff --git a/src/routes/v1/doramas/dramanice/DramaniceRoutes.ts b/src/routes/v1/doramas/dramanice/DramaniceRoutes.ts index f4df7f51..eedaa2bd 100644 --- a/src/routes/v1/doramas/dramanice/DramaniceRoutes.ts +++ b/src/routes/v1/doramas/dramanice/DramaniceRoutes.ts @@ -5,28 +5,31 @@ const router = Router(); // Filter router.get("/anime/animelatinohd/filter", async (req, res) => { - const { search, type, page, year, genre } = req.query - - const data = await Dorama.GetAnimeByFilter(search as string, type as unknown as number, page as unknown as number, year as string, genre as string) - res.send(data) + const { search, type, page, year, genre } = req.query; + + const data = await Dorama.GetAnimeByFilter( + search as string, + type as unknown as number, + page as unknown as number, + year as string, + genre as string, + ); + res.send(data); }); // Anime Info +(Episodes list) router.get("/anime/animelatinohd/name/:name", async (req, res) => { - - const { name } = req.params - const data = await Dorama.GetAnimeInfo(name) - res.send(data) - + const { name } = req.params; + const data = await Dorama.GetAnimeInfo(name); + res.send(data); }); // Episode Info +(Video Servers) router.get("/anime/animelatinohd/episode/:episode", async (req, res) => { - const { lang } = req.query - const { episode } = req.params - const data = await Dorama.GetEpisodeServers(episode, lang as string) - res.send(data) - + const { lang } = req.query; + const { episode } = req.params; + const data = await Dorama.GetEpisodeServers(episode, lang as string); + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/manga/comick/ComickRoutes.ts b/src/routes/v1/manga/comick/ComickRoutes.ts index 357db755..69e2c1fb 100644 --- a/src/routes/v1/manga/comick/ComickRoutes.ts +++ b/src/routes/v1/manga/comick/ComickRoutes.ts @@ -3,31 +3,34 @@ import { Comick } from "../../../../scraper/sites/manga/comick/Comick"; const Manga = new Comick(); const router = Router(); - router.get("/manga/comick/filter", async (req, res) => { - const { search, type, year, genre } = req.query; + const { search, type, year, genre } = req.query; - const data = await Manga.GetMangaByFilter(search as string, type as unknown as number, year as string, genre as string) + const data = await Manga.GetMangaByFilter( + search as string, + type as unknown as number, + year as string, + genre as string, + ); - res.send(data) + res.send(data); }); - router.get("/manga/comick/title/:manga", async (req, res) => { - const { manga } = req.params; - const { lang } = req.query; + const { manga } = req.params; + const { lang } = req.query; - const data = await Manga.GetMangaInfo(manga, lang as string) + const data = await Manga.GetMangaInfo(manga, lang as string); - res.send(data) + res.send(data); }); router.get("/manga/comick/chapter/:chapter", async (req, res) => { - const { chapter } = req.params - const { lang } = req.query; + const { chapter } = req.params; + const { lang } = req.query; - const data = await Manga.GetChapterInfo(chapter, lang as string) + const data = await Manga.GetChapterInfo(chapter, lang as string); - res.send(data) + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/manga/inmanga/InmangaRoutes.ts b/src/routes/v1/manga/inmanga/InmangaRoutes.ts index 74ec504d..55594112 100644 --- a/src/routes/v1/manga/inmanga/InmangaRoutes.ts +++ b/src/routes/v1/manga/inmanga/InmangaRoutes.ts @@ -3,30 +3,31 @@ import { Inmanga } from "../../../../scraper/sites/manga/inmanga/Inmanga"; const Manga = new Inmanga(); const router = Router(); - router.get("/manga/inmanga/filter", async (req, res) => { - const { search, type, genre } = req.query; - const data = await Manga.GetMangaByFilter(search as string, type as unknown as number, genre as string[]); - - res.send(data) + const { search, type, genre } = req.query; + const data = await Manga.GetMangaByFilter( + search as string, + type as unknown as number, + genre as string[], + ); + + res.send(data); }); - router.get("/manga/inmanga/title/:manga", async (req, res) => { - const { manga } = req.params; - const {cid} = req.query; + const { manga } = req.params; + const { cid } = req.query; - const data = await Manga.GetMangaInfo(manga, cid as string); + const data = await Manga.GetMangaInfo(manga, cid as string); - res.send(data) + res.send(data); }); router.get("/manga/inmanga/chapter/:chapter", async (req, res) => { + const { chapter } = req.params; + const { cid } = req.query; + const data = await Manga.GetChapterInfo(chapter, cid as string); - const { chapter } = req.params - const { cid } = req.query - const data = await Manga.GetChapterInfo(chapter, cid as string); - - res.send(data) + res.send(data); }); -export default router \ No newline at end of file +export default router; diff --git a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts index d494d99b..93de587e 100644 --- a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts +++ b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts @@ -17,15 +17,19 @@ router.get(`/manga/${manganelo.name}/filter`, async (req, res) => { const result = await manganelo.Filter({ sts: req.query.status as unknown as "ongoing" | "completed", genres: req.query.genres as unknown as string, - orby: req.query.order as unknown as typeof manganatoOrderByOptionsList[number], - page: req.query.page as unknown as number + orby: req.query + .order as unknown as (typeof manganatoOrderByOptionsList)[number], + page: req.query.page as unknown as number, }); return res.status(200).send(result); -}) +}); router.get(`/manga/${manganelo.name}/chapter/:id`, async (req, res) => { - const result = await manganelo.GetMangaChapters(req.params.id as unknown as string, req.query.num as unknown as number); + const result = await manganelo.GetMangaChapters( + req.params.id as unknown as string, + req.query.num as unknown as number, + ); return res.status(200).send(result); }); diff --git a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts index 11e11988..359ebdef 100644 --- a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts +++ b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts @@ -6,7 +6,7 @@ import { MangaReaderFilterScore, MangaReaderFilterSort, MangaReaderFilterStatus, - MangaReaderFilterType + MangaReaderFilterType, } from "../../../../scraper/sites/manga/MangaReader/MangaReaderTypes"; const mangaReader = new MangaReader(); @@ -31,7 +31,8 @@ router.get("/manga/mangareader/filter", async (req, res) => { const status = req.query.status as MangaReaderFilterStatus; const ratingType = req.query.rating as MangaReaderFilterRatingType; const score = req.query.score as MangaReaderFilterScore; - const language = req.query.language as typeof MangaReaderFilterLanguage[number]; + const language = req.query + .language as (typeof MangaReaderFilterLanguage)[number]; const startYear = req.query.startyear as unknown as number; const startMonth = req.query.startmonth as unknown as number; const startDay = req.query.startday as unknown as number; @@ -54,7 +55,7 @@ router.get("/manga/mangareader/filter", async (req, res) => { endMonth, endDay, sort, - numPage + numPage, }); return res.status(200).send(data); @@ -68,14 +69,14 @@ router.get("/manga/mangareader/chapter/:id", async (req, res) => { try { const id = req.params.id as unknown as number; const chapterNumber = req.query.number as unknown as number; - const language = req.query.lang as typeof MangaReaderFilterLanguage[number]; - + const language = req.query + .lang as (typeof MangaReaderFilterLanguage)[number]; const data = await mangaReader.GetMangaChapters( id, chapterNumber, language, - "chapter" + "chapter", ); return res.status(200).send(data); @@ -89,14 +90,14 @@ router.get("/manga/mangareader/volume/:id", async (req, res) => { try { const id = req.params.id as unknown as number; const chapterNumber = req.query.number as unknown as number; - const language = req.query.lang as typeof MangaReaderFilterLanguage[number]; - + const language = req.query + .lang as (typeof MangaReaderFilterLanguage)[number]; const data = await mangaReader.GetMangaChapters( id, chapterNumber, language, - "volume" + "volume", ); return res.status(200).send(data); diff --git a/src/scraper/sites/anime/9Anime/9Anime.js b/src/scraper/sites/anime/9Anime/9Anime.js index 6a3a27e2..abe1b5cd 100644 --- a/src/scraper/sites/anime/9Anime/9Anime.js +++ b/src/scraper/sites/anime/9Anime/9Anime.js @@ -45,7 +45,7 @@ async function NineAnimeInfo(animeName) { $("div.binfo div.info div.bmeta").each((i, e) => { const info = parseAnimeInfo( $(e).find("div.meta:first").text().trim(), - $(e).find("div.meta").next().text().trim() + $(e).find("div.meta").next().text().trim(), ); animeData.year = info.dateAired.trim(); animeData.genres = info.genre; diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index 671cba08..fdaff2be 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -8,6 +8,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains @@ -19,14 +20,14 @@ import { * */ -export class AnimeBlix { +export class AnimeBlix extends AnimeProviderModel { readonly url = "https://vwv.animeblix.org"; readonly api = "https://api.animelatinohd.com"; async GetAnimeInfo(anime: string): Promise { try { const { data } = await axios.get( - `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}` + `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}`, ); const $ = cheerio.load(data); @@ -37,7 +38,7 @@ export class AnimeBlix { ? $(".cn .info .r .u li span[class='fi']").text() : $(".cn .info .r .u li span[class='es']").text(); const AnimeDate = $( - ".cn .info .r .u li span:contains('Fecha de emisión:')" + ".cn .info .r .u li span:contains('Fecha de emisión:')", ) .next() .text() @@ -105,7 +106,7 @@ export class AnimeBlix { const ReplaceSymbols: RegExp = /(,)+/g; const ListEpisode = ListEpisodeIndex.slice( ListEpisodeIndex.indexOf("var eps = "), - ListEpisodeIndex.indexOf(";> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 87bd3914..4c4e3fe2 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -14,7 +14,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; export class AnimeFlv extends AnimeProviderModel { readonly url = "https://animeflv.ws"; @@ -58,7 +58,7 @@ export class AnimeFlv extends AnimeProviderModel { episode.name = $(e).children(".Title").text().trim(); episode.url = `/anime/flv/episode/${`${l}`.replace( "/anime", - "/anime/flv" + "/anime/flv", )}`; episode.number = $(e).children("p").last().text().trim(); episode.image = $(e).children("figure").find(".lazy").attr("src"); @@ -68,10 +68,10 @@ export class AnimeFlv extends AnimeProviderModel { } catch (error) { console.log( "An error occurred while getting the anime info: invalid name", - error + error, ); throw new Error( - "An error occurred while getting the anime info: invalid name" + "An error occurred while getting the anime info: invalid name", ); } } @@ -83,7 +83,7 @@ export class AnimeFlv extends AnimeProviderModel { status?: StatusAnimeflv, ord?: OrderAnimeflv, page?: number, - title?: string + title?: string, ): Promise> { try { const { data } = await axios.get(`${this.url}/browse`, { @@ -144,7 +144,7 @@ export class AnimeFlv extends AnimeProviderModel { servers.url = videoData; if (videoData.includes("streaming.php")) { await this.getM3U( - `${videoData.replace("streaming.php", "ajax.php")}&refer=none` + `${videoData.replace("streaming.php", "ajax.php")}&refer=none`, ).then((g) => { if (g.source.length) { servers.file_url = g.source[0].file; diff --git a/src/scraper/sites/anime/animeflv/animeflv_helper.ts b/src/scraper/sites/anime/animeflv/animeflv_helper.ts index f5be52c1..601b7f57 100644 --- a/src/scraper/sites/anime/animeflv/animeflv_helper.ts +++ b/src/scraper/sites/anime/animeflv/animeflv_helper.ts @@ -1,52 +1,52 @@ //genres animeflf export enum Genres { - Action = "Acción", - MartialArts = "Artes Marciales", - Adventure = "Aventuras", - Racing = "Carreras", - ScienceFiction = "Ciencia Ficción", - Comedy = "Comedia", - Dementia = "Demencia", - Demons = "Demonios", - Sports = "Deportes", - Drama = "Drama", - Ecchi = "Ecchi", - School = "Escolares", - Space = "Espacial", - Fantasy = "Fantasía", - Harem = "Harem", - Historical = "Histórico", - Kids = "Infantil", - Josei = "Josei", - Games = "Juegos", - Magic = "Magia", - Mecha = "Mecha", - Military = "Militar", - Mystery = "Misterio", - Music = "Música", - Parody = "Parodia", - Police = "Policía", - Psychological = "Psicológico", - SliceOfLife = "Recuentos de la vida", - Romance = "Romance", - Samurai = "Samurai", - Seinen = "Seinen", - Shoujo = "Shoujo", - Shounen = "Shounen", - Supernatural = "Sobrenatural", - Superpowers = "Superpoderes", - Suspense = "Suspenso", - Horror = "Terror", - Vampires = "Vampiros", - Yaoi = "Yaoi", - Yuri = "Yuri", - } + Action = "Acción", + MartialArts = "Artes Marciales", + Adventure = "Aventuras", + Racing = "Carreras", + ScienceFiction = "Ciencia Ficción", + Comedy = "Comedia", + Dementia = "Demencia", + Demons = "Demonios", + Sports = "Deportes", + Drama = "Drama", + Ecchi = "Ecchi", + School = "Escolares", + Space = "Espacial", + Fantasy = "Fantasía", + Harem = "Harem", + Historical = "Histórico", + Kids = "Infantil", + Josei = "Josei", + Games = "Juegos", + Magic = "Magia", + Mecha = "Mecha", + Military = "Militar", + Mystery = "Misterio", + Music = "Música", + Parody = "Parodia", + Police = "Policía", + Psychological = "Psicológico", + SliceOfLife = "Recuentos de la vida", + Romance = "Romance", + Samurai = "Samurai", + Seinen = "Seinen", + Shoujo = "Shoujo", + Shounen = "Shounen", + Supernatural = "Sobrenatural", + Superpowers = "Superpoderes", + Suspense = "Suspenso", + Horror = "Terror", + Vampires = "Vampiros", + Yaoi = "Yaoi", + Yuri = "Yuri", +} - export enum StatusAnimeflv { - OnGoing = "En emision", - Finished = "Finalizado", - Upcoming = "Próximamente", - } +export enum StatusAnimeflv { + OnGoing = "En emision", + Finished = "Finalizado", + Upcoming = "Próximamente", +} export type TypeAnimeflv = "all" | 1 | 2 | 3 | 4; -export type OrderAnimeflv = "all" | 1 | 2 | 3 | 4 | 5; \ No newline at end of file +export type OrderAnimeflv = "all" | 1 | 2 | 3 | 4 | 5; diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 9f5b2352..10b84353 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -8,6 +8,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; export class AnimeLatinoHD extends AnimeProviderModel { readonly url = "https://www.animelatinohd.com"; @@ -132,8 +133,8 @@ export class AnimeLatinoHD extends AnimeProviderModel { Server.url = "https://filemoon.sx" + "/e/" + id_file }*/ AnimeEpisodeInfo.servers.push(Server); - } - ) + }, + ), ); return AnimeEpisodeInfo; @@ -147,7 +148,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { type?: number, page?: number, year?: string, - genre?: string + genre?: string, ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index 68c899d4..d594368c 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -2,84 +2,121 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; -import { AnimeSearch, ResultSearch, IResultSearch, IAnimeSearch } from "../../../../types/search"; +import { + AnimeSearch, + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "../../../../types/search"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains - * + * * https://animevostfr.tv - * -*/ - -export class Animevostfr { - readonly url = "https://animevostfr.tv"; - readonly api = "https://api.animelatinohd.com"; - - async GetAnimeInfo(anime: string): Promise { - try { - const { data } = await axios.get(`${this.url}/${anime}`); - const $ = cheerio.load(data); - - - const AnimeTypes = $(".mvic-info .mvici-right p strong:contains(' Type:')").nextAll().text() - const AnimeStatus = $(".mvic-info .mvici-right p a[rel='tag']").first().text() - const AnimeDate = $(".mvic-info .mvici-right p strong:contains(' An:')").nextAll().text() - const AnimeDescription = $(".mvi-content .mvic-desc .desc p").html() - - - - - const AnimeInfo: Anime = { - name: $(".mvi-content .mvic-desc h1").text(), - url: `/anime/animevostfr/name/${anime}`, - synopsis: AnimeDescription.slice(AnimeDescription.indexOf("Synopsis:") + "Synopsis:".length, -1).trim(), - alt_name: [...AnimeDescription.slice(AnimeDescription.indexOf("Titre alternatif:") + "Titre alternatif:".length, AnimeDescription.indexOf("Synopsis:")).replace("
\n", "").split("/").map((n) => n.replace(/^\s+|\s+$|\s+(?=\s)/g, ""))], - image: { - url: $(".mvi-content .mvic-thumb img").attr("data-lazy-src") - }, - genres: [...$(".mvic-info .mvici-left p").first().text().replace("\n Genres:\n ", "").split(",").map((n) => n.replace(/^\s+|\s+$|\s+(?=\s)/g, ""))], - type: AnimeTypes == "Anime" ? "Anime" : AnimeTypes == "MOVIE" ? "Movie" : "Null", //tv,pelicula,especial,ova - status: AnimeStatus == "En cours" ? true : false, - date: AnimeDate ? { year: AnimeDate } : null, - episodes: [] - } - - $("#seasonss .les-title").each((_i, e) => { - - const number = $(e).find("a").attr("href").substring($(e).find("a").attr("href").lastIndexOf("-") + 1).replace("/", "") - const AnimeEpisode: Episode = { - name: "Episode " + number, - number: number, - image: "", - url: `/anime/animevostfr/episode/${anime + "-" + number}` - } - - AnimeInfo.episodes.push(AnimeEpisode); - }) - - return AnimeInfo; - - } catch (error) { - console.log(error) - } + * + */ + +export class Animevostfr extends AnimeProviderModel { + readonly url = "https://animevostfr.tv"; + readonly api = "https://api.animelatinohd.com"; + + async GetAnimeInfo(anime: string): Promise { + try { + const { data } = await axios.get(`${this.url}/${anime}`); + const $ = cheerio.load(data); + + const AnimeTypes = $( + ".mvic-info .mvici-right p strong:contains(' Type:')", + ) + .nextAll() + .text(); + const AnimeStatus = $(".mvic-info .mvici-right p a[rel='tag']") + .first() + .text(); + const AnimeDate = $(".mvic-info .mvici-right p strong:contains(' An:')") + .nextAll() + .text(); + const AnimeDescription = $(".mvi-content .mvic-desc .desc p").html(); + + const AnimeInfo: Anime = { + name: $(".mvi-content .mvic-desc h1").text(), + url: `/anime/animevostfr/name/${anime}`, + synopsis: AnimeDescription.slice( + AnimeDescription.indexOf("Synopsis:") + "Synopsis:".length, + -1, + ).trim(), + alt_name: [ + ...AnimeDescription.slice( + AnimeDescription.indexOf("Titre alternatif:") + + "Titre alternatif:".length, + AnimeDescription.indexOf("Synopsis:"), + ) + .replace("
\n", "") + .split("/") + .map((n) => n.replace(/^\s+|\s+$|\s+(?=\s)/g, "")), + ], + image: { + url: $(".mvi-content .mvic-thumb img").attr("data-lazy-src"), + }, + genres: [ + ...$(".mvic-info .mvici-left p") + .first() + .text() + .replace("\n Genres:\n ", "") + .split(",") + .map((n) => n.replace(/^\s+|\s+$|\s+(?=\s)/g, "")), + ], + type: + AnimeTypes == "Anime" + ? "Anime" + : AnimeTypes == "MOVIE" + ? "Movie" + : "Null", //tv,pelicula,especial,ova + status: AnimeStatus == "En cours" ? true : false, + date: AnimeDate ? { year: AnimeDate } : null, + episodes: [], + }; + + $("#seasonss .les-title").each((_i, e) => { + const number = $(e) + .find("a") + .attr("href") + .substring($(e).find("a").attr("href").lastIndexOf("-") + 1) + .replace("/", ""); + const AnimeEpisode: Episode = { + name: "Episode " + number, + number: number, + image: "", + url: `/anime/animevostfr/episode/${anime + "-" + number}`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.log(error); } - async GetEpisodeServers(episode: string): Promise { - try { - - const number = episode.substring(episode.lastIndexOf("-") + 1) - const anime = episode.substring(0, episode.lastIndexOf("-")) - - const { data } = await axios.get(`${this.url}/episode/${anime}-episode-${number}`); - const $ = cheerio.load(data); - const s = $('.form-group.list-server select option') - const e = $('.list-episodes select option') - const ListFilmId = [] - const ListServer = [] - - s.map((_i, e) => ListServer.push($(e).val())) - e.map((_i, e) => ListFilmId.push($(e).attr("episodeid"))) - - /* + } + async GetEpisodeServers(episode: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + + const { data } = await axios.get( + `${this.url}/episode/${anime}-episode-${number}`, + ); + const $ = cheerio.load(data); + const s = $(".form-group.list-server select option"); + const e = $(".list-episodes select option"); + const ListFilmId = []; + const ListServer = []; + + s.map((_i, e) => ListServer.push($(e).val())); + e.map((_i, e) => ListFilmId.push($(e).attr("episodeid"))); + + /* "SERVER_VIP" "SERVER_HYDRAX" "SERVER_PHOTOSS" @@ -94,73 +131,89 @@ export class Animevostfr { "SERVER_RAPID_VIDEO" */ - const AnimeEpisodeInfo: Episode = { - name: "Episode " + number, - url: `/anime/animevostfr/episode/${episode}`, - number: number, - image: "", - servers: [] - } - - await Promise.all(ListServer.map(async (n) => { - const servers = await axios.get(`${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId[0]}`) - let currentData = servers.data - if (n == "opencdn" || n == "photo") { - currentData = currentData.replace("?logo=https://animevostfr.tv/1234.png", "").replace("short.ink/", "abysscdn.com/?v=") - } - const Servers: EpisodeServer = { - name: n, - url: currentData, - } - AnimeEpisodeInfo.servers.push(Servers) - })) - - - - AnimeEpisodeInfo.servers.sort((a: EpisodeServer, b: EpisodeServer) => a.name.length - b.name.length) - return AnimeEpisodeInfo; - } catch (error) { - console.log(error) - } + const AnimeEpisodeInfo: Episode = { + name: "Episode " + number, + url: `/anime/animevostfr/episode/${episode}`, + number: number, + image: "", + servers: [], + }; + + await Promise.all( + ListServer.map(async (n) => { + const servers = await axios.get( + `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId[0]}`, + ); + let currentData = servers.data; + if (n == "opencdn" || n == "photo") { + currentData = currentData + .replace("?logo=https://animevostfr.tv/1234.png", "") + .replace("short.ink/", "abysscdn.com/?v="); + } + const Servers: EpisodeServer = { + name: n, + url: currentData, + }; + AnimeEpisodeInfo.servers.push(Servers); + }), + ); + + AnimeEpisodeInfo.servers.sort( + (a: EpisodeServer, b: EpisodeServer) => a.name.length - b.name.length, + ); + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); } - - async GetAnimeByFilter(search?: string, type?: number, page?: number, year?: string, genre?: string): Promise> { - try { - const { data } = await axios.get(`${this.api}/api/anime/list`, { - params: { - search: search, - type: type, - year: year, - genre: genre, - page: page - } - }); - - const animeSearchParseObj = data - - const animeSearch: ResultSearch = { - nav: { - count: animeSearchParseObj.data.length, - current: animeSearchParseObj.current_page, - next: animeSearchParseObj.data.length < 28 ? 0 : animeSearchParseObj.current_page + 1, - hasNext: animeSearchParseObj.data.length < 28 ? false : true - }, - results: [] - } - animeSearchParseObj.data.map(e => { - const animeSearchData: AnimeSearch = { - name: e.name, - image: "https://www.themoviedb.org/t/p/original" + e.poster + "?&w=53&q=95", - url: `/anime/animelatinohd/name/${e.slug}`, - type: "" - } - animeSearch.results.push(animeSearchData) - }) - return animeSearch; - } catch (error) { - console.log(error) - } + } + + async GetAnimeByFilter( + search?: string, + type?: number, + page?: number, + year?: string, + genre?: string, + ): Promise> { + try { + const { data } = await axios.get(`${this.api}/api/anime/list`, { + params: { + search: search, + type: type, + year: year, + genre: genre, + page: page, + }, + }); + + const animeSearchParseObj = data; + + const animeSearch: ResultSearch = { + nav: { + count: animeSearchParseObj.data.length, + current: animeSearchParseObj.current_page, + next: + animeSearchParseObj.data.length < 28 + ? 0 + : animeSearchParseObj.current_page + 1, + hasNext: animeSearchParseObj.data.length < 28 ? false : true, + }, + results: [], + }; + animeSearchParseObj.data.map((e) => { + const animeSearchData: AnimeSearch = { + name: e.name, + image: + "https://www.themoviedb.org/t/p/original" + + e.poster + + "?&w=53&q=95", + url: `/anime/animelatinohd/name/${e.slug}`, + type: "", + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.log(error); } - + } } - diff --git a/src/scraper/sites/anime/gogoanime/assets/getAllAnimesHTML.ts b/src/scraper/sites/anime/gogoanime/assets/getAllAnimesHTML.ts index a89ebfe4..5735a252 100644 --- a/src/scraper/sites/anime/gogoanime/assets/getAllAnimesHTML.ts +++ b/src/scraper/sites/anime/gogoanime/assets/getAllAnimesHTML.ts @@ -1,77 +1,57 @@ import { IAnime } from "@animetypes/anime"; import { getHTML } from "./getHTML"; - - export async function getAllAnimes(url: string, numPage: number) { - - try { - - + try { let animes: IAnime[] = []; - - let $ = await getHTML( - `${url}?page=${numPage}` - ); - - let pageState = $(".anime_name h2").text(). - replace("ADVERTISEMENTSRECENT RELEASESeason", ""). - trim(); + let $ = await getHTML(`${url}?page=${numPage}`); - if ( pageState != "404 Not found" ) { + let pageState = $(".anime_name h2") + .text() + .replace("ADVERTISEMENTSRECENT RELEASESeason", "") + .trim(); + if (pageState != "404 Not found") { $(".last_episodes ul li").each((_, element) => { + let animeName = $(element).find("p.name").find("a").text().trim(); + let animeImage = $(element) + .find(".img") + .find("a") + .find("img") + .attr("src"); - let animeName = $(element).find("p.name").find("a"). - text(). - trim(); - - - let animeImage = $(element).find(".img").find("a"). - find("img"). - attr("src"); + let animeNameUrl = $(element) + .find(".img") + .find("a") + .attr("href") + .replace("/category/", ""); - let animeNameUrl= $(element).find(".img").find("a"). - attr("href"). - replace("/category/", ""); - - let year: string | number = $(element).find("p.released"). - text(). - replace("Released: ", ""); + let year: string | number = $(element) + .find("p.released") + .text() + .replace("Released: ", ""); year = parseInt(year); - animes.push({ - name: animeName, - image: { - url: animeImage + animes.push({ + name: animeName, + image: { + url: animeImage, + }, + url: `/anime/gogoanime/name/${animeNameUrl}`, + date: { + begin: { + year: year, }, - url: `/anime/gogoanime/name/${animeNameUrl}`, - date: { - begin: { - year: year, - } - } - - }) - - - }) - + }, + }); + }); } - - - - return animes - - } catch( error ){ - - return error - - } - - + return animes; + } catch (error) { + return error; + } } diff --git a/src/scraper/sites/anime/gogoanime/assets/getHTML.ts b/src/scraper/sites/anime/gogoanime/assets/getHTML.ts index 5d2b6e96..81670458 100644 --- a/src/scraper/sites/anime/gogoanime/assets/getHTML.ts +++ b/src/scraper/sites/anime/gogoanime/assets/getHTML.ts @@ -2,15 +2,8 @@ import { load } from "cheerio"; import axios from "axios"; export async function getHTML(url: string) { - const { data } = await axios.get(`${url}`); const pageHTML = load(data); return pageHTML; - } - - - - - diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index 30f28b50..6acbd4a5 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -2,168 +2,203 @@ import axios from "axios"; import * as cheerio from "cheerio"; import { api, utils } from "../../../../types/utils"; import * as types from "../../../../types/."; -import { ResultSearch, IResultSearch, IAnimeSearch } from "../../../../types/search"; +import { + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "../../../../types/search"; const PageInfo = { - name: 'monoschinos', - url: 'https://monoschinos2.com', // url page - server: 'monoschinos2', - domain: 'monoschinos2.com' -} + name: "monoschinos", + url: "https://monoschinos2.com", // url page + server: "monoschinos2", + domain: "monoschinos2.com", +}; /** * This function returns a list of servers where the episode is located. * The URLs of the servers are Base64 encoded. - * + * * @param url * @returns */ async function getEpisodeServers(url: string): Promise { - let servers: types.EpisodeServer[] = []; - const $ = cheerio.load((await axios.get(url)).data); - $('div.playother').children().each((_i, element) => { - servers.push(new types.EpisodeServer($(element).text().trim(), - Buffer.from($(element).attr('data-player'), 'base64').toString('binary')) - ); + let servers: types.EpisodeServer[] = []; + const $ = cheerio.load((await axios.get(url)).data); + $("div.playother") + .children() + .each((_i, element) => { + servers.push( + new types.EpisodeServer( + $(element).text().trim(), + Buffer.from($(element).attr("data-player"), "base64").toString( + "binary", + ), + ), + ); }); - return servers; + return servers; } /** - * - * @param $ - * @param element - * @returns + * + * @param $ + * @param element + * @returns */ async function getEpisodeByElement($, element): Promise { - const episode = new types.Episode(); - episode.number = parseInt($(element).find('div.positioning p').text().trim()); - episode.image = $(element).find('div.animeimgdiv img.animeimghv').attr('data-src'); - episode.name = $(element).find('h2.animetitles').text().trim(); - episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); - return episode; + const episode = new types.Episode(); + episode.number = parseInt($(element).find("div.positioning p").text().trim()); + episode.image = $(element) + .find("div.animeimgdiv img.animeimghv") + .attr("data-src"); + episode.name = $(element).find("h2.animetitles").text().trim(); + episode.url = api.getEpisodeURL(PageInfo, $(element).find("a").attr("href")); + return episode; } /** - * + * * @throws {Error} - * @returns + * @returns */ async function getLastEpisodes(): Promise { - let episodes: types.Episode[] = []; - const $ = cheerio.load((await axios.get(PageInfo.url)).data); - const elements = $('div.heroarea div.heroarea1 div.row').children(); - for (let i = 0; i < elements.length; i++) { - if ($(elements[i]).children().length != 0) { - episodes.push(await getEpisodeByElement($, elements[i])); - } + let episodes: types.Episode[] = []; + const $ = cheerio.load((await axios.get(PageInfo.url)).data); + const elements = $("div.heroarea div.heroarea1 div.row").children(); + for (let i = 0; i < elements.length; i++) { + if ($(elements[i]).children().length != 0) { + episodes.push(await getEpisodeByElement($, elements[i])); } - return episodes; + } + return episodes; } /** - * - * @param $ - * @returns + * + * @param $ + * @returns */ function getGenres($): string[] { - let genres: string[] = []; - $('div.chapterdetls2 table tbody a').each((_i, element) => { - genres.push($(element).text().trim()) - }); - return genres; + let genres: string[] = []; + $("div.chapterdetls2 table tbody a").each((_i, element) => { + genres.push($(element).text().trim()); + }); + return genres; } /** - * - * @param $ - * @returns + * + * @param $ + * @returns */ function getAnimeEpisodes($): types.Episode[] { - let episodes: types.Episode[] = []; - $('div.heromain2 div.allanimes div.row').children().each((_i, element) => { - const episode = new types.Episode(); - episode.number = parseInt($(element).attr('data-episode').trim()); - episode.image = $(element).find('img.animeimghv').attr('data-src'); - episode.name = $(element).find('img.animeimghv').attr('alt'); - episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); - episodes.push(episode); + let episodes: types.Episode[] = []; + $("div.heromain2 div.allanimes div.row") + .children() + .each((_i, element) => { + const episode = new types.Episode(); + episode.number = parseInt($(element).attr("data-episode").trim()); + episode.image = $(element).find("img.animeimghv").attr("data-src"); + episode.name = $(element).find("img.animeimghv").attr("alt"); + episode.url = api.getEpisodeURL( + PageInfo, + $(element).find("a").attr("href"), + ); + episodes.push(episode); }); - return episodes; + return episodes; } const calendar = [ - [['enero', 'febrero', 'marzo'], 'invierno'], - [['abril', 'mayo', 'junio'], 'primavera'], - [['julio', 'agosto', 'septiembre'], 'verano'], - [['octubre', 'noviembre', 'diciembre'], 'otoño'], + [["enero", "febrero", "marzo"], "invierno"], + [["abril", "mayo", "junio"], "primavera"], + [["julio", "agosto", "septiembre"], "verano"], + [["octubre", "noviembre", "diciembre"], "otoño"], ]; interface ClimaticCalendar { - year: number; - station: null | string; + year: number; + station: null | string; } /** - * The calendar of the anime is extracted. The format shown on the + * The calendar of the anime is extracted. The format shown on the * website is 'dd from mm from yyyy'. - * - * @param element + * + * @param element * @returns the calendar of anime */ function getAnimeCalendar(element): ClimaticCalendar { - const date = element.find('ol.breadcrumb li.breadcrumb-item').text().trim().split(' '); - if (date.length != 5) - return { year: 0, station: null }; - else { - for (let i = 0; i < calendar.length; i++) { - if (calendar[i][0].includes(date[2].toLowerCase())) { - return { year: parseInt(date.pop()), station: calendar[i][1].toString() }; - } - } + const date = element + .find("ol.breadcrumb li.breadcrumb-item") + .text() + .trim() + .split(" "); + if (date.length != 5) return { year: 0, station: null }; + else { + for (let i = 0; i < calendar.length; i++) { + if (calendar[i][0].includes(date[2].toLowerCase())) { + return { + year: parseInt(date.pop()), + station: calendar[i][1].toString(), + }; + } } + } } /** - * - * @param url - * @returns + * + * @param url + * @returns */ async function getAnime(url: string): Promise { - // The anime page in monoschinos does not define the chronology and type - const $ = cheerio.load((await axios.get(url)).data); - const calendar = getAnimeCalendar($($('div.chapterdetails nav').children()[1])); - const anime = new types.Anime(); - anime.name = $('div.chapterdetails').find('h1').text(); - anime.alt_name = $('div.chapterdetails').find('span.alterno').text(); - anime.url = api.getAnimeURL(PageInfo, url); - anime.synopsis = $('div.chapterdetls2 p').text().trim(); - anime.genres = getGenres($); - anime.image = new types.Image($('div.chapterpic img').attr('src'), $('div.herobg img').attr('src')); - anime.status = 'estreno' === $('div.butns button.btn1').text().toLowerCase().trim(); - anime.episodes = getAnimeEpisodes($); - anime.date = new types.Calendar(calendar.year); - anime.station = calendar.station; - return anime; + // The anime page in monoschinos does not define the chronology and type + const $ = cheerio.load((await axios.get(url)).data); + const calendar = getAnimeCalendar( + $($("div.chapterdetails nav").children()[1]), + ); + const anime = new types.Anime(); + anime.name = $("div.chapterdetails").find("h1").text(); + anime.alt_name = $("div.chapterdetails").find("span.alterno").text(); + anime.url = api.getAnimeURL(PageInfo, url); + anime.synopsis = $("div.chapterdetls2 p").text().trim(); + anime.genres = getGenres($); + anime.image = new types.Image( + $("div.chapterpic img").attr("src"), + $("div.herobg img").attr("src"), + ); + anime.status = + "estreno" === $("div.butns button.btn1").text().toLowerCase().trim(); + anime.episodes = getAnimeEpisodes($); + anime.date = new types.Calendar(calendar.year); + anime.station = calendar.station; + return anime; } /** - * + * * @throws {Error} - * @param url - * @returns + * @param url + * @returns */ async function getLastAnimes(url?: string): Promise { - let animes: types.Anime[] = []; - const $ = cheerio.load((await axios.get(url ?? `${PageInfo.url}/emision`)).data); - const elements = $('div.heroarea div.heromain div.row').children(); - for (let i = 0; i < elements.length; i++) { - const href = $(elements[i]).find('a').attr('href'); - if (utils.isUsableValue(href) && href !== 'https://monoschinos2.com/emision?p=2') { - animes.push(await getAnime(href)); - } + let animes: types.Anime[] = []; + const $ = cheerio.load( + (await axios.get(url ?? `${PageInfo.url}/emision`)).data, + ); + const elements = $("div.heroarea div.heromain div.row").children(); + for (let i = 0; i < elements.length; i++) { + const href = $(elements[i]).find("a").attr("href"); + if ( + utils.isUsableValue(href) && + href !== "https://monoschinos2.com/emision?p=2" + ) { + animes.push(await getAnime(href)); } - return animes; + } + return animes; } //console.log(await getLastAnimes('https://monoschinos2.com/animes?categoria=anime&genero=accion&fecha=2023&letra=A')); @@ -171,32 +206,41 @@ async function getLastAnimes(url?: string): Promise { //console.log(await getLastAnimes()) /** - * - * + * + * * @author Zukaritasu */ -export class Monoschinos -{ - getLastEpisodes = getLastEpisodes; - getLastAnimes = getLastEpisodes; - getEpisodeServers = getEpisodeServers; - getAnime = getAnime; - - async filter(name: (string | null), category?: string, genre?: string, year?: string, letter?: string): Promise> { - const animes = new ResultSearch(); - const link = utils.isUsableValue(name) ? `${PageInfo.url}/buscar?q=${name}` : - `${PageInfo.url}/animes?categoria=${category ?? false}&genero=${genre ?? false}&fecha=${year ?? false}&letra=${letter ?? false}`; - (await getLastAnimes(link)) - .forEach(element => { - if (utils.isUsableValue(element)) { - animes.results.push({ name: element.name, image: element.image.url, - url: element.url, type: category - }); - } - }); - return animes; - } -}; - +export class Monoschinos { + getLastEpisodes = getLastEpisodes; + getLastAnimes = getLastEpisodes; + getEpisodeServers = getEpisodeServers; + getAnime = getAnime; + + async filter( + name: string | null, + category?: string, + genre?: string, + year?: string, + letter?: string, + ): Promise> { + const animes = new ResultSearch(); + const link = utils.isUsableValue(name) + ? `${PageInfo.url}/buscar?q=${name}` + : `${PageInfo.url}/animes?categoria=${category ?? false}&genero=${ + genre ?? false + }&fecha=${year ?? false}&letra=${letter ?? false}`; + (await getLastAnimes(link)).forEach((element) => { + if (utils.isUsableValue(element)) { + animes.results.push({ + name: element.name, + image: element.image.url, + url: element.url, + type: category, + }); + } + }); + return animes; + } +} -//console.log(await getAnime("https://monoschinos2.com/anime/world-dai-star-sub-espanol")); \ No newline at end of file +//console.log(await getAnime("https://monoschinos2.com/anime/world-dai-star-sub-espanol")); diff --git a/src/scraper/sites/anime/otakuTV/getAnime.js b/src/scraper/sites/anime/otakuTV/getAnime.js index f78ebc95..5086ee54 100644 --- a/src/scraper/sites/anime/otakuTV/getAnime.js +++ b/src/scraper/sites/anime/otakuTV/getAnime.js @@ -1,6 +1,6 @@ import axios from "axios"; import * as ch from "cheerio"; -import { Anime } from '../../../../utils/schemaProviders.js'; +import { Anime } from "../../../../utils/schemaProviders.js"; async function getAnime(anime) { //aplica minuscula y reemplaza espacios con - @@ -8,7 +8,7 @@ async function getAnime(anime) { try { const { data } = await axios.get( - `https://www1.otakustv.com/anime/${animename}` + `https://www1.otakustv.com/anime/${animename}`, ); console.log(data); @@ -17,20 +17,16 @@ async function getAnime(anime) { const anime = new Anime(); - anime.name = $("div.inn-text h1.text-white").text(); - if ($("span.btn-anime-info").text().trim() == 'Finalizado') { + if ($("span.btn-anime-info").text().trim() == "Finalizado") { anime.active = false; } else { anime.active = true; } - anime.synopsis = $("div.modal-body").first().text().trim(); - anime.year = $("span.date") - .text() - .replace(" Estreno: ", "Se estreno: "); + anime.year = $("span.date").text().replace(" Estreno: ", "Se estreno: "); // anime.rate = $("div.none-otakus-a span.ml-1").text().replace("-", " -"); //Aqui literalmente tuve que usar un each no mas para sacar una cosa, @@ -41,13 +37,11 @@ async function getAnime(anime) { }); const getEpisodes = $( - "div.tabs div.tab-content div.tab-pane div.pl-lg-4 div.container-fluid div.row div.col-6 " + "div.tabs div.tab-content div.tab-pane div.pl-lg-4 div.container-fluid div.row div.col-6 ", ).each((i, j) => { anime.episodes.push({ title: $(j).find("p").find("span").html(), - url: $(j) - .find("a") - .attr("href") + url: $(j).find("a").attr("href"), }); }); @@ -59,6 +53,6 @@ async function getAnime(anime) { } } -getAnime('bocchi the rock'); +getAnime("bocchi the rock"); export default { getAnime }; diff --git a/src/scraper/sites/anime/otakuTV/getAnimeComingSoon.js b/src/scraper/sites/anime/otakuTV/getAnimeComingSoon.js index 1574aac4..29c54342 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimeComingSoon.js +++ b/src/scraper/sites/anime/otakuTV/getAnimeComingSoon.js @@ -9,7 +9,7 @@ export async function getComingSoon() { const animes = []; const getAnimes = $( - "div.pronto div.base-carusel div.carusel_pronto div.item " + "div.pronto div.base-carusel div.carusel_pronto div.item ", ).each((i, element) => { animes.push({ name: $(element).find("h2").text().trim(), diff --git a/src/scraper/sites/anime/otakuTV/getAnimeInfo.js b/src/scraper/sites/anime/otakuTV/getAnimeInfo.js index a5a89193..cdd0ecf5 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimeInfo.js +++ b/src/scraper/sites/anime/otakuTV/getAnimeInfo.js @@ -1,6 +1,6 @@ import axios from "axios"; import * as ch from "cheerio"; -import { Anime } from '../../../../utils/schemaProviders.js'; +import { Anime } from "../../../../utils/schemaProviders.js"; async function getAnime(anime) { //Transform to minus and change spaces to - @@ -8,7 +8,7 @@ async function getAnime(anime) { try { const { data } = await axios.get( - `https://www1.otakustv.com/anime/${animename}` + `https://www1.otakustv.com/anime/${animename}`, ); const $ = ch.load(data); @@ -19,7 +19,7 @@ async function getAnime(anime) { anime.name = $("div.inn-text h1.text-white").text(); //Test its state - if ($("span.btn-anime-info").text().trim() == 'Finalizado') { + if ($("span.btn-anime-info").text().trim() == "Finalizado") { anime.active = false; } else { anime.active = true; @@ -29,10 +29,7 @@ async function getAnime(anime) { anime.synopsis = $("div.modal-body").first().text().trim(); //gets year - anime.year = $("span.date") - .text() - .replace(" Estreno: ", "Se estreno: "); - + anime.year = $("span.date").text().replace(" Estreno: ", "Se estreno: "); //omits first thing and return its image const stuff = $("div.img-in img ").each((i, j) => { @@ -41,22 +38,18 @@ async function getAnime(anime) { //pushing episodes on its array const getEpisodes = $( - "div.tabs div.tab-content div.tab-pane div.pl-lg-4 div.container-fluid div.row div.col-6 " + "div.tabs div.tab-content div.tab-pane div.pl-lg-4 div.container-fluid div.row div.col-6 ", ).each((i, j) => { anime.episodes.push({ title: $(j).find("p").find("span").html(), - url: $(j) - .find("a") - .attr("href") + url: $(j).find("a").attr("href"), }); }); - return anime; } catch (error) { return error; } } - export default { getAnime }; diff --git a/src/scraper/sites/anime/otakuTV/getAnimeNew.js b/src/scraper/sites/anime/otakuTV/getAnimeNew.js index 10cfe247..efaea6ee 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimeNew.js +++ b/src/scraper/sites/anime/otakuTV/getAnimeNew.js @@ -23,7 +23,7 @@ async function getAnimeNew() { .replace("video(s)", "") .trim(), }); - } + }, ); return animes; diff --git a/src/scraper/sites/anime/otakuTV/getAnimePremiere.js b/src/scraper/sites/anime/otakuTV/getAnimePremiere.js index 70927f9c..9f1b8bbc 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimePremiere.js +++ b/src/scraper/sites/anime/otakuTV/getAnimePremiere.js @@ -1,2 +1,2 @@ import axios from "axios"; -import * as ch from 'cheerio' +import * as ch from "cheerio"; diff --git a/src/scraper/sites/anime/otakuTV/getAnimeRanking.js b/src/scraper/sites/anime/otakuTV/getAnimeRanking.js index 7fa758c6..5df72dce 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimeRanking.js +++ b/src/scraper/sites/anime/otakuTV/getAnimeRanking.js @@ -9,12 +9,15 @@ async function getAnimeRanking() { const animeRanking = []; const title = $( - "div.ranking div.base-carusel div.carusel_ranking div.item " + "div.ranking div.base-carusel div.carusel_ranking div.item ", ).each((i, j) => { animeRanking.push({ title: $(j).find("a").find("h2").text(), coverImg: $(j).find("a").find("img").attr("src"), - linkTo: $(j).find("a").attr("href").replace("https://www1.otakustv.com/anime/", "/anime/otakuTV/"), + linkTo: $(j) + .find("a") + .attr("href") + .replace("https://www1.otakustv.com/anime/", "/anime/otakuTV/"), }); }); diff --git a/src/scraper/sites/anime/otakuTV/getAnimeServer.js b/src/scraper/sites/anime/otakuTV/getAnimeServer.js index 6d1167b7..74ef39cb 100644 --- a/src/scraper/sites/anime/otakuTV/getAnimeServer.js +++ b/src/scraper/sites/anime/otakuTV/getAnimeServer.js @@ -1,41 +1,26 @@ import { load } from "cheerio"; import puppeteer from "puppeteer"; - async function getAnimeServer(name) { - - const animeName = name?.toLowerCase().replace(/\s/g, "-"); try { - - - const browser = await puppeteer.launch(); const page = await browser.newPage(); - await page.goto("https://www1.otakustv.com/anime/bocchi-the-rock/episodio-1"); + await page.goto( + "https://www1.otakustv.com/anime/bocchi-the-rock/episodio-1", + ); const html = await page.content(); - const $ = load(html); console.log($.html()); - - - - - - - - - } catch (error) { - return error + return error; } } -getAnimeServer() - +getAnimeServer(); -export default { getAnimeServer } +export default { getAnimeServer }; diff --git a/src/scraper/sites/anime/otakuTV/getUsersActive.js b/src/scraper/sites/anime/otakuTV/getUsersActive.js index 87d139fa..c28307f7 100644 --- a/src/scraper/sites/anime/otakuTV/getUsersActive.js +++ b/src/scraper/sites/anime/otakuTV/getUsersActive.js @@ -18,7 +18,7 @@ async function getUsersActive() { .attr("href") .replace( "https://www1.otakustv.com/perfil/", - "/anime/otakuTV/profile/" + "/anime/otakuTV/profile/", ), name: $(j).find("h2").text(), ranking: $(j).find("p").text(), diff --git a/src/scraper/sites/anime/otakuTV/search.js b/src/scraper/sites/anime/otakuTV/search.js index 1bb40ec8..bbf3f2c6 100644 --- a/src/scraper/sites/anime/otakuTV/search.js +++ b/src/scraper/sites/anime/otakuTV/search.js @@ -1,14 +1,15 @@ -import { load } from 'cheerio'; -import axios from 'axios'; -import { AnimeSearch, Image, SearchArray } from '../../../../utils/schemaProviders.js'; - +import { load } from "cheerio"; +import axios from "axios"; +import { + AnimeSearch, + Image, + SearchArray, +} from "../../../../utils/schemaProviders.js"; async function Search(name) { - let url = `https://www1.otakustv.com/buscador?q=${name}`; try { - const { data } = await axios.get(`${url}`); const $ = load(data); @@ -16,21 +17,20 @@ async function Search(name) { const animes = new SearchArray(1); //Get all animes - $('.animes_lista .row .col-6').each((i, element) => { - animes.data.push(new AnimeSearch( - $(element).find('p.font-GDSherpa-Bold').text(), - new Image($(element).find('img').attr('src')), - $(element).find('a').attr('href') - )) - }) - - return animes - + $(".animes_lista .row .col-6").each((i, element) => { + animes.data.push( + new AnimeSearch( + $(element).find("p.font-GDSherpa-Bold").text(), + new Image($(element).find("img").attr("src")), + $(element).find("a").attr("href"), + ), + ); + }); + + return animes; } catch (error) { - return error + return error; } - } -export default { Search } - +export default { Search }; diff --git a/src/scraper/sites/anime/tioanime/TioAnime.ts b/src/scraper/sites/anime/tioanime/TioAnime.ts index d9392b89..55ce4f81 100644 --- a/src/scraper/sites/anime/tioanime/TioAnime.ts +++ b/src/scraper/sites/anime/tioanime/TioAnime.ts @@ -2,268 +2,321 @@ import axios from "axios"; import * as cheerio from "cheerio"; import { utils } from "../../../../types/utils"; import * as types from "../../../../types/."; -import { ResultSearch, IResultSearch, IAnimeSearch } from "../../../../types/search"; +import { + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "../../../../types/search"; const PageInfo = { - url: 'https://tioanime.com' // url page -} - + url: "https://tioanime.com", // url page +}; function getAnimeChronology($) { - let chrono_list: types.IChronology[] = []; - $('section.w-history ul.list-unstyled li').each((_i, element) => { - // The chronological anime has to access its year and type as extra - // information that is not included in the Chronology class - chrono_list.push(new types.Chronology( - $(element).find('h3.title').text(), - PageInfo.url + $(element).find('div.media-body a').attr('href'), - PageInfo.url + $(element).find('figure.fa-play-circle img').attr('src') - )); - }); - return chrono_list; + let chrono_list: types.IChronology[] = []; + $("section.w-history ul.list-unstyled li").each((_i, element) => { + // The chronological anime has to access its year and type as extra + // information that is not included in the Chronology class + chrono_list.push( + new types.Chronology( + $(element).find("h3.title").text(), + PageInfo.url + $(element).find("div.media-body a").attr("href"), + PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src"), + ), + ); + }); + return chrono_list; } async function getEpisodeServers(url) { - 'use strict' - let servers: types.IEpisodeServer[] = []; - const $ = cheerio.load((await axios.get(url)).data); - const script = $($('script').get().pop()).text().trim(); - try { - const videos = new Function(script.substring(0, script.indexOf('$(document)')) - .replace("var videos =", "return"))(); - for (let i = 0; i < videos.length; i++) { - servers.push(new types.EpisodeServer(videos[i][0], - videos[i][1].replace('\\', '') - )); - } + "use strict"; + let servers: types.IEpisodeServer[] = []; + const $ = cheerio.load((await axios.get(url)).data); + const script = $($("script").get().pop()).text().trim(); + try { + const videos = new Function( + script + .substring(0, script.indexOf("$(document)")) + .replace("var videos =", "return"), + )(); + for (let i = 0; i < videos.length; i++) { + servers.push( + new types.EpisodeServer(videos[i][0], videos[i][1].replace("\\", "")), + ); + } - const table_downloads = $($('table.table-downloads tbody')).children(); - for (let i = 0; i < table_downloads.length; i++) { - const server = $($(table_downloads[i]).find('td')[0]).text().trim(); - const episode_server = servers.find((episode) => { - return episode.name.toLowerCase() === server.toLocaleLowerCase(); - }); - if (!(episode_server == undefined || episode_server == null)) { - episode_server.file_url = $(table_downloads[i]).find("a").attr('href'); - } else { - servers.push({ - name: server, url: null, file_url: $(table_downloads[i]).find("a").attr('href') - }); - } - } - } catch (error) { - console.log(error) + const table_downloads = $($("table.table-downloads tbody")).children(); + for (let i = 0; i < table_downloads.length; i++) { + const server = $($(table_downloads[i]).find("td")[0]).text().trim(); + const episode_server = servers.find((episode) => { + return episode.name.toLowerCase() === server.toLocaleLowerCase(); + }); + if (!(episode_server == undefined || episode_server == null)) { + episode_server.file_url = $(table_downloads[i]).find("a").attr("href"); + } else { + servers.push({ + name: server, + url: null, + file_url: $(table_downloads[i]).find("a").attr("href"), + }); + } } - return servers; + } catch (error) { + console.log(error); + } + return servers; } async function getAnimeEpisodes(data) { - let __episodes: types.IEpisode[] = []; - data.episodes.forEach(episode_number => { - let episode = new types.Episode(); - episode.name = `${data.info[2]} Capitulo ${episode_number}`; - episode.image = PageInfo.url +`/uploads/thumbs/${data.info[0]}.jpg`; - episode.url = `/anime/tioanime/episode/${data.info[1]}-${episode_number}`; - episode.number = episode_number; - __episodes.push(episode); - }); - return __episodes; + let __episodes: types.IEpisode[] = []; + data.episodes.forEach((episode_number) => { + let episode = new types.Episode(); + episode.name = `${data.info[2]} Capitulo ${episode_number}`; + episode.image = PageInfo.url + `/uploads/thumbs/${data.info[0]}.jpg`; + episode.url = `/anime/tioanime/episode/${data.info[1]}-${episode_number}`; + episode.number = episode_number; + __episodes.push(episode); + }); + return __episodes; } function getEpisode($, element) { - const title = $(element).find('h3.title').text().trim(); - const episode = new types.Episode(); - episode.image = PageInfo.url + $(element).find('figure.fa-play-circle img').attr('src'); - episode.url = $(element).find('article.episode a').attr('href').replace('/ver/', '/anime/tioanime/servers/') - - for (let i = title.length - 1; i >= 0; i--) { - if (title[i] == ' ') { - episode.name = title.substring(0, i).trim(); - episode.number = parseInt(title.substring(i + 1, title.length)); - break; - } + const title = $(element).find("h3.title").text().trim(); + const episode = new types.Episode(); + episode.image = + PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src"); + episode.url = $(element) + .find("article.episode a") + .attr("href") + .replace("/ver/", "/anime/tioanime/servers/"); + + for (let i = title.length - 1; i >= 0; i--) { + if (title[i] == " ") { + episode.name = title.substring(0, i).trim(); + episode.number = parseInt(title.substring(i + 1, title.length)); + break; } - return episode; + } + return episode; } async function getLastEpisodes() { - let episodes: types.IEpisode[] = []; - try { - const $ = cheerio.load((await axios.get(PageInfo.url)).data); - const elements = $('div.container section ul.episodes li').children(); - for (let i = 0; i < elements.length; i++) { - episodes.push(getEpisode($, elements[i])); - } - } catch (error) { - console.log(error); + let episodes: types.IEpisode[] = []; + try { + const $ = cheerio.load((await axios.get(PageInfo.url)).data); + const elements = $("div.container section ul.episodes li").children(); + for (let i = 0; i < elements.length; i++) { + episodes.push(getEpisode($, elements[i])); } - return episodes; + } catch (error) { + console.log(error); + } + return episodes; } function getGenres($, elements) { - let genres: string[] = []; - elements.each((_i, element) => { - genres.push($(element).find('a').text().trim()); - }); - return genres; + let genres: string[] = []; + elements.each((_i, element) => { + genres.push($(element).find("a").text().trim()); + }); + return genres; } function getScriptAnimeInfo($) { - let script = $($('script').get().pop()).text().trim(); - try { - script = script.substring(0, script.indexOf('$(document)')); - script = script.substring(0, script.indexOf("var episodes_details")) - + "return { info: anime_info, episodes: episodes };"; - const variables = new Function(script)() - return { info: variables.info, episodes: variables.episodes }; - } catch (error) { - console.log(error + "\n Script code: " + script); - } - return null; + let script = $($("script").get().pop()).text().trim(); + try { + script = script.substring(0, script.indexOf("$(document)")); + script = + script.substring(0, script.indexOf("var episodes_details")) + + "return { info: anime_info, episodes: episodes };"; + const variables = new Function(script)(); + return { info: variables.info, episodes: variables.episodes }; + } catch (error) { + console.log(error + "\n Script code: " + script); + } + return null; } async function getAnime(url) { - // ignore property alt_name - const $ = cheerio.load((await axios.get(url)).data); - const data = getScriptAnimeInfo($); - // It is possible that the object returned by the getScriptAnimeInfo function is null. - if (data == null) - throw new Error('The getScriptAnimeInfo() function returns a null value.'); - const anime = new types.Anime(); - anime.name = $('div.container h1.title').text(); - //anime.url = url; - anime.url = url.replace('https://tioanime.com/anime/', '/anime/tioanime/name/'); - //anime.type = $('div.meta span.anime-type-peli').text(); - anime.type = (() => { - switch ($('div.meta span.anime-type-peli').text().toLowerCase()) { - case "anime": - return "Anime"; - case "movie": - return "Movie"; - case "one": - return "ONA"; - case "ona": - return "OVA"; - } - return "Null"; - })(); - - //anime.year = parseInt($('div.meta span.year').text().trim()); - anime.date = new types.DatePeriod(new types.Calendar(data.info.length < 4 ? - parseInt($('div.meta span.year').text().trim().substring(0, 4)) : - new Date(data.info[3]).getFullYear()) - ); - anime.synopsis = $('p.sinopsis').text().trim(); - anime.genres = getGenres($, $('div.container p.genres span')); - anime.image = new types.Image(PageInfo.url + $('div.container div.thumb figure img').attr('src'), - $('figure.backdrop img').attr('src') == undefined ? "" : - PageInfo.url + $('figure.backdrop img').attr('src') - ); - anime.status = $('div.thumb a.status').text().trim() === 'En emision'; - anime.station = $('div.meta span.fa-snowflake').text().trim().split('\n')[0]; - anime.episodes = await getAnimeEpisodes(data); - anime.chronology = getAnimeChronology($); - return anime; + // ignore property alt_name + const $ = cheerio.load((await axios.get(url)).data); + const data = getScriptAnimeInfo($); + // It is possible that the object returned by the getScriptAnimeInfo function is null. + if (data == null) + throw new Error("The getScriptAnimeInfo() function returns a null value."); + const anime = new types.Anime(); + anime.name = $("div.container h1.title").text(); + //anime.url = url; + anime.url = url.replace( + "https://tioanime.com/anime/", + "/anime/tioanime/name/", + ); + //anime.type = $('div.meta span.anime-type-peli').text(); + anime.type = (() => { + switch ($("div.meta span.anime-type-peli").text().toLowerCase()) { + case "anime": + return "Anime"; + case "movie": + return "Movie"; + case "one": + return "ONA"; + case "ona": + return "OVA"; + } + return "Null"; + })(); + + //anime.year = parseInt($('div.meta span.year').text().trim()); + anime.date = new types.DatePeriod( + new types.Calendar( + data.info.length < 4 + ? parseInt($("div.meta span.year").text().trim().substring(0, 4)) + : new Date(data.info[3]).getFullYear(), + ), + ); + anime.synopsis = $("p.sinopsis").text().trim(); + anime.genres = getGenres($, $("div.container p.genres span")); + anime.image = new types.Image( + PageInfo.url + $("div.container div.thumb figure img").attr("src"), + $("figure.backdrop img").attr("src") == undefined + ? "" + : PageInfo.url + $("figure.backdrop img").attr("src"), + ); + anime.status = $("div.thumb a.status").text().trim() === "En emision"; + anime.station = $("div.meta span.fa-snowflake").text().trim().split("\n")[0]; + anime.episodes = await getAnimeEpisodes(data); + anime.chronology = getAnimeChronology($); + return anime; } async function getLastAnimes(url: string) { - console.log(url) - try { - let animes: types.IAnime[] = []; - const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); - const elements = $(utils.isUsableValue(url) ? 'ul.animes' : 'div.container section ul.list-unstyled.row li').children(); - for (let i = 0; i < elements.length; i++) { - const anime_url = $(elements[i]).find('article.anime a').attr('href'); - if (utils.isUsableValue(anime_url)) { - animes.push(await getAnime(PageInfo.url + anime_url)); - } - } - return animes; - } catch (error) { - console.log(error); + console.log(url); + try { + let animes: types.IAnime[] = []; + const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); + const elements = $( + utils.isUsableValue(url) + ? "ul.animes" + : "div.container section ul.list-unstyled.row li", + ).children(); + for (let i = 0; i < elements.length; i++) { + const anime_url = $(elements[i]).find("article.anime a").attr("href"); + if (utils.isUsableValue(anime_url)) { + animes.push(await getAnime(PageInfo.url + anime_url)); + } } - return []; + return animes; + } catch (error) { + console.log(error); + } + return []; } async function getSectionContents(section: number) { - let animes: types.IAnime[] = []; - try { - const $ = cheerio.load((await axios.get(`${PageInfo.url}/directorio?type%5B%5D=${section}`)).data); - const elements = $(`ul.animes`).children(); - for (let i = 0; i < elements.length; i++) { - animes.push(await getAnime(PageInfo.url + $(elements[i]) - .find('article.anime a').attr('href'))); - } - } catch (error) { - console.log(error); + let animes: types.IAnime[] = []; + try { + const $ = cheerio.load( + (await axios.get(`${PageInfo.url}/directorio?type%5B%5D=${section}`)) + .data, + ); + const elements = $(`ul.animes`).children(); + for (let i = 0; i < elements.length; i++) { + animes.push( + await getAnime( + PageInfo.url + $(elements[i]).find("article.anime a").attr("href"), + ), + ); } - return animes; + } catch (error) { + console.log(error); + } + return animes; } async function getLastMovies() { - return await getSectionContents(1); + return await getSectionContents(1); } async function getLastOvas() { - return await getSectionContents(2); + return await getSectionContents(2); } async function getLastOnas() { - return await getSectionContents(3); + return await getSectionContents(3); } - - export interface IYearRange { - begin: number; - end: number; + begin: number; + end: number; } -export class TioAnime -{ - getLastEpisodes = getLastEpisodes; - getLastAnimes = getLastAnimes; - getLastMovies = getLastMovies; - getLastOvas = getLastOvas; - getLastOnas = getLastOnas; - getEpisodeServers = getEpisodeServers; - getAnime = getAnime; +export class TioAnime { + getLastEpisodes = getLastEpisodes; + getLastAnimes = getLastAnimes; + getLastMovies = getLastMovies; + getLastOvas = getLastOvas; + getLastOnas = getLastOnas; + getEpisodeServers = getEpisodeServers; + getAnime = getAnime; - private arrayToURLParams(param: string, array: string[]): string { - let elements = ''; - if (utils.isUsableValue(array)) { - for (let i = 0; i < array.length; i++) { - elements += `${param}%5B%5D=${array[i]}`; - if (i + 1 < array.length) { - elements += '&'; - } - } - } - return elements.length !== 0 ? elements + '&' : ''; - } + private arrayToURLParams(param: string, array: string[]): string { + let elements = ""; + if (utils.isUsableValue(array)) { + for (let i = 0; i < array.length; i++) { + elements += `${param}%5B%5D=${array[i]}`; + if (i + 1 < array.length) { + elements += "&"; + } + } + } + return elements.length !== 0 ? elements + "&" : ""; + } - // 0: Anime (TV), 1 Movie, 2: OVA, 3: ONA - // all genres - // year_range { begin, end } - // 2: Finalizado, 1: En emision, 3: Proximamente - // recent, -recent - - async filter(name: (string | null), types?: string[], genres?: string[], year_range?: IYearRange, status?: number, sort?: string): - Promise> { - const animes = new ResultSearch(); - let usable; - if (!(usable = utils.isUsableValue(name) && name.trim().length != 0)) - year_range ?? (year_range = { begin: 1950, end: new Date().getFullYear() }); - (await getLastAnimes(`${PageInfo.url}/directorio?${(usable ? `q=${name}` : `${this.arrayToURLParams('type', types)}${this.arrayToURLParams('genero', genres)}year=${year_range.begin}%2C${year_range.end}&status=${status ?? 2}&sort=${sort ?? 'recent'}`)}`)) - .forEach(element => { - if (utils.isUsableValue(element)) { - animes.results.push({ name: element.name, image: element.image.url, - url: element.url, type: element.type }) - } - }); - return animes; - } -}; + // 0: Anime (TV), 1 Movie, 2: OVA, 3: ONA + // all genres + // year_range { begin, end } + // 2: Finalizado, 1: En emision, 3: Proximamente + // recent, -recent + + async filter( + name: string | null, + types?: string[], + genres?: string[], + year_range?: IYearRange, + status?: number, + sort?: string, + ): Promise> { + const animes = new ResultSearch(); + let usable; + if (!(usable = utils.isUsableValue(name) && name.trim().length != 0)) + year_range ?? + (year_range = { begin: 1950, end: new Date().getFullYear() }); + ( + await getLastAnimes( + `${PageInfo.url}/directorio?${ + usable + ? `q=${name}` + : `${this.arrayToURLParams("type", types)}${this.arrayToURLParams( + "genero", + genres, + )}year=${year_range.begin}%2C${year_range.end}&status=${ + status ?? 2 + }&sort=${sort ?? "recent"}` + }`, + ) + ).forEach((element) => { + if (utils.isUsableValue(element)) { + animes.results.push({ + name: element.name, + image: element.image.url, + url: element.url, + type: element.type, + }); + } + }); + return animes; + } +} /*getEpisodeServers('https://tioanime.com/ver/oniichan-wa-oshimai-3').then(result => { console.log(result) @@ -271,4 +324,3 @@ export class TioAnime //new TioAnime().filter(null, null, ["demencia"], null, null, null).then(result => { console.log(result) } ) /* */ - diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index 210741ec..2b1ba91a 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -9,6 +9,7 @@ import { AnimeSearch, } from "../../../../types/search"; import { UnPacked } from "../../../../types/utils"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; /** List of Domains * https://wcostream.tv @@ -49,10 +50,10 @@ export class WcoStream extends AnimeProviderModel { const $ = cheerio.load(data); const image = $( - "#category_description .ui-grid-solo .ui-block-a img" + "#category_description .ui-grid-solo .ui-block-a img", ).attr("src"); const name = $( - ".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x" + ".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x", ) .text() .replace("Share On", ""); @@ -94,7 +95,7 @@ export class WcoStream extends AnimeProviderModel { data.includes("English Dubbed") ? "English Dubbed" : "English Subbed", - "" + "", ) .replace("Episode", "") .trim() @@ -143,7 +144,7 @@ export class WcoStream extends AnimeProviderModel { const anime = episode.substring(0, episode.lastIndexOf("-")); const { data } = await axios.get( - `https://www.wcostream.tv/playlist-cat/${anime}` + `https://www.wcostream.tv/playlist-cat/${anime}`, ); const $ = cheerio.load(data); @@ -153,7 +154,7 @@ export class WcoStream extends AnimeProviderModel { .trim() .slice(mainUrl.search("playlist:") + 6, mainUrl.search("image: ") - 4) .trim() - .replace(",", "") + .replace(",", ""), ); const mainData = await axios.get(this.url + mainOrigin); @@ -161,7 +162,7 @@ export class WcoStream extends AnimeProviderModel { mainData.data .replaceAll(":image", " type='image'") .replaceAll(":source", " type='video'") - .trim() + .trim(), ); const AnimeEpisodeInfo: Episode = { @@ -223,7 +224,7 @@ export class WcoStream extends AnimeProviderModel { async GetAnimeByFilter( search?: string, - page?: number + page?: number, ): Promise> { try { const formdata = new FormData(); @@ -277,8 +278,8 @@ export class WcoStream extends AnimeProviderModel { const RequestBR = await eval( UnBuffer.slice( UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, - UnBuffer.indexOf("}],image:", 1) - ) + UnBuffer.indexOf("}],image:", 1), + ), ); return RequestBR; diff --git a/src/scraper/sites/anime/zoro/Zoro.ts b/src/scraper/sites/anime/zoro/Zoro.ts index 1d47cdef..517db05e 100644 --- a/src/scraper/sites/anime/zoro/Zoro.ts +++ b/src/scraper/sites/anime/zoro/Zoro.ts @@ -7,7 +7,7 @@ import { ResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "src/scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; export class Zoro extends AnimeProviderModel { readonly url = "https://aniwatch.to"; @@ -71,7 +71,7 @@ export class Zoro extends AnimeProviderModel { language?: string, sort?: string, genres?: string, - page_anime?: string + page_anime?: string, ) { try { const { data } = await axios.get(`${this.url}/filter`, { @@ -125,7 +125,7 @@ export class Zoro extends AnimeProviderModel { Referer: `https://zoro.to/watch/${animename + "-" + ep}`, "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }, - } + }, ); const $ = load(data.html); const epi = new Episode(); @@ -172,7 +172,7 @@ export class Zoro extends AnimeProviderModel { private async getServers(id): Promise { const { data } = await axios.get( - `${this.url}/ajax/v2/episode/sources?id=${id}` + `${this.url}/ajax/v2/episode/sources?id=${id}`, ); return data; } diff --git a/src/scraper/sites/doramas/dramanice/Dramanice.ts b/src/scraper/sites/doramas/dramanice/Dramanice.ts index 35fef897..a8087c4b 100644 --- a/src/scraper/sites/doramas/dramanice/Dramanice.ts +++ b/src/scraper/sites/doramas/dramanice/Dramanice.ts @@ -2,99 +2,117 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; -import { AnimeSearch, ResultSearch, IResultSearch, IAnimeSearch } from "../../../../types/search"; +import { + AnimeSearch, + ResultSearch, + IResultSearch, + IAnimeSearch, +} from "../../../../types/search"; export class Dramanice { - readonly url = "https://www.animelatinohd.com"; - readonly api = "https://api.animelatinohd.com"; - - async GetAnimeInfo(anime: string): Promise { - try { - const { data } = await axios.get(`${this.url}/anime/${anime}`); - const $ = cheerio.load(data); - - const animeInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data - - const AnimeInfo: Anime = { - name: animeInfoParseObj.name, - url: `/anime/animelatinohd/name/${anime}`, - synopsis: animeInfoParseObj.overview, - alt_name: [...animeInfoParseObj.name_alternative.split(",")], - image: { - url: "https://www.themoviedb.org/t/p/original" + animeInfoParseObj.poster + "?&w=53&q=95" - }, - genres: [...animeInfoParseObj.genres.split(",")], - type: animeInfoParseObj.type, - status: animeInfoParseObj.status == 1 ? "En emisión" : "Finalizado", - date: animeInfoParseObj.aired, - episodes: [] - } - - animeInfoParseObj.episodes.map(e => { - const AnimeEpisode: Episode = { - name: animeInfoParseObj.name, - number: e.number + "", - image: "https://www.themoviedb.org/t/p/original" + animeInfoParseObj.banner + "?&w=280&q=95", - url: `/anime/animelatinohd/episode/${animeInfoParseObj.slug + "-" + e.number}` - } - - AnimeInfo.episodes.push(AnimeEpisode); - }) - - return AnimeInfo; - - } catch (error) { - console.log(error) - } + readonly url = "https://www.animelatinohd.com"; + readonly api = "https://api.animelatinohd.com"; + + async GetAnimeInfo(anime: string): Promise { + try { + const { data } = await axios.get(`${this.url}/anime/${anime}`); + const $ = cheerio.load(data); + + const animeInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props + .pageProps.data; + + const AnimeInfo: Anime = { + name: animeInfoParseObj.name, + url: `/anime/animelatinohd/name/${anime}`, + synopsis: animeInfoParseObj.overview, + alt_name: [...animeInfoParseObj.name_alternative.split(",")], + image: { + url: + "https://www.themoviedb.org/t/p/original" + + animeInfoParseObj.poster + + "?&w=53&q=95", + }, + genres: [...animeInfoParseObj.genres.split(",")], + type: animeInfoParseObj.type, + status: animeInfoParseObj.status == 1 ? "En emisión" : "Finalizado", + date: animeInfoParseObj.aired, + episodes: [], + }; + + animeInfoParseObj.episodes.map((e) => { + const AnimeEpisode: Episode = { + name: animeInfoParseObj.name, + number: e.number + "", + image: + "https://www.themoviedb.org/t/p/original" + + animeInfoParseObj.banner + + "?&w=280&q=95", + url: `/anime/animelatinohd/episode/${ + animeInfoParseObj.slug + "-" + e.number + }`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.log(error); } - async GetEpisodeServers(episode: string, lang: string): Promise { - try { - - const number = episode.substring(episode.lastIndexOf("-") + 1) - const anime = episode.substring(0, episode.lastIndexOf("-")) - const langType = [{ lang: "es", type: "Latino" }, { lang: "jp", type: "Subtitulado" }] - - const { data } = await axios.get(`${this.url}/ver/${anime}/${number}`); - const $ = cheerio.load(data); - - const animeEpisodeParseObj = JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data - - const AnimeEpisodeInfo: Episode = { - name: animeEpisodeParseObj.anime.name, - url: `/anime/animelatinohd/episode/${episode}`, - number: number, - image: "", - servers: [] - } - - const sel_lang = langType.filter((e) => e.lang == lang) - let f_index = 0 - - if (sel_lang.length) { - $("#languaje option").each((_i, e) => { - if ($(e).text() == sel_lang[0].type) { - f_index = Number($(e).val()) - } - }) - } else { - $("#languaje option").each((_i, e) => { - f_index = Number($(e).val()) - }) - } - - await Promise.all(animeEpisodeParseObj.players[f_index].map(async (e: { server: { title: string; }; id: string; }) => { - //let min = await axios.get("https://api.animelatinohd.com/stream/" + e.id, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62", "Referer": "https://www.animelatinohd.com/" } }) - // let dat = cheerio.load(min.data) - - const Server: EpisodeServer = { - name: e.server.title, - url: "", - } - Server.url = "https://api.animelatinohd.com/stream/" + e.id - Server.name = e.server.title - - //state 1 - /*if (e.server.title == "Beta") { + } + async GetEpisodeServers(episode: string, lang: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + const langType = [ + { lang: "es", type: "Latino" }, + { lang: "jp", type: "Subtitulado" }, + ]; + + const { data } = await axios.get(`${this.url}/ver/${anime}/${number}`); + const $ = cheerio.load(data); + + const animeEpisodeParseObj = JSON.parse($("#__NEXT_DATA__").html()).props + .pageProps.data; + + const AnimeEpisodeInfo: Episode = { + name: animeEpisodeParseObj.anime.name, + url: `/anime/animelatinohd/episode/${episode}`, + number: number, + image: "", + servers: [], + }; + + const sel_lang = langType.filter((e) => e.lang == lang); + let f_index = 0; + + if (sel_lang.length) { + $("#languaje option").each((_i, e) => { + if ($(e).text() == sel_lang[0].type) { + f_index = Number($(e).val()); + } + }); + } else { + $("#languaje option").each((_i, e) => { + f_index = Number($(e).val()); + }); + } + + await Promise.all( + animeEpisodeParseObj.players[f_index].map( + async (e: { server: { title: string }; id: string }) => { + //let min = await axios.get("https://api.animelatinohd.com/stream/" + e.id, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62", "Referer": "https://www.animelatinohd.com/" } }) + // let dat = cheerio.load(min.data) + + const Server: EpisodeServer = { + name: e.server.title, + url: "", + }; + Server.url = "https://api.animelatinohd.com/stream/" + e.id; + Server.name = e.server.title; + + //state 1 + /*if (e.server.title == "Beta") { let sel = dat("script:contains('var foo_ui = function (event) {')") let sort = String(sel.html()) let domain = eval(sort.slice(sort.search("const url"), sort.search("const langDef")).replace("const url =", "").trim()) @@ -113,53 +131,64 @@ export class Dramanice { let id_file = sortMORE.replace("_x", "") Server.url = "https://filemoon.sx" + "/e/" + id_file }*/ - AnimeEpisodeInfo.servers.push(Server) - })) - - return AnimeEpisodeInfo; - } catch (error) { - console.log(error) - } + AnimeEpisodeInfo.servers.push(Server); + }, + ), + ); + + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); } - - async GetAnimeByFilter(search?: string, type?: number, page?: number, year?: string, genre?: string): Promise> { - try { - const { data } = await axios.get(`${this.api}/api/anime/list`, { - params: { - search: search, - type: type, - year: year, - genre: genre, - page: page - } - }); - - const animeSearchParseObj = data - - const animeSearch: ResultSearch = { - nav: { - count: animeSearchParseObj.data.length, - current: animeSearchParseObj.current_page, - next: animeSearchParseObj.data.length < 28 ? 0 : animeSearchParseObj.current_page + 1, - hasNext: animeSearchParseObj.data.length < 28 ? false : true - }, - results: [] - } - animeSearchParseObj.data.map(e => { - const animeSearchData: AnimeSearch = { - name: e.name, - image: "https://www.themoviedb.org/t/p/original" + e.poster + "?&w=53&q=95", - url: `/anime/animelatinohd/name/${e.slug}`, - type: "" - } - animeSearch.results.push(animeSearchData) - }) - return animeSearch; - } catch (error) { - console.log(error) - } + } + + async GetAnimeByFilter( + search?: string, + type?: number, + page?: number, + year?: string, + genre?: string, + ): Promise> { + try { + const { data } = await axios.get(`${this.api}/api/anime/list`, { + params: { + search: search, + type: type, + year: year, + genre: genre, + page: page, + }, + }); + + const animeSearchParseObj = data; + + const animeSearch: ResultSearch = { + nav: { + count: animeSearchParseObj.data.length, + current: animeSearchParseObj.current_page, + next: + animeSearchParseObj.data.length < 28 + ? 0 + : animeSearchParseObj.current_page + 1, + hasNext: animeSearchParseObj.data.length < 28 ? false : true, + }, + results: [], + }; + animeSearchParseObj.data.map((e) => { + const animeSearchData: AnimeSearch = { + name: e.name, + image: + "https://www.themoviedb.org/t/p/original" + + e.poster + + "?&w=53&q=95", + url: `/anime/animelatinohd/name/${e.slug}`, + type: "", + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.log(error); } - + } } - - diff --git a/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts b/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts index b2b5a73f..d04a3b87 100644 --- a/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts +++ b/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts @@ -60,10 +60,10 @@ export class MangaBuddy { const dateText = $(e).find("time.chapter-update").text().trim(); //date string const yearMangaVerification = Number.isNaN( - Number(dateText.split(" ")[2]) + Number(dateText.split(" ")[2]), ); const dayMangaVerification = Number.isNaN( - Number(dateText.split(" ")[0]) + Number(dateText.split(" ")[0]), ); let monthAbbr; diff --git a/src/scraper/sites/manga/MangaReader/MangaReader.ts b/src/scraper/sites/manga/MangaReader/MangaReader.ts index 552d61a2..b17c1e61 100644 --- a/src/scraper/sites/manga/MangaReader/MangaReader.ts +++ b/src/scraper/sites/manga/MangaReader/MangaReader.ts @@ -3,14 +3,14 @@ import { IMangaResult, Manga, MangaChapter, - MangaVolume + MangaVolume, } from "../../../../types/manga"; import axios from "axios"; import { load } from "cheerio"; import { MangaReaderFilterLanguage, MangaReaderChapterType, - MangaReaderFilterData + MangaReaderFilterData, } from "./MangaReaderTypes"; import { IResultSearch, ResultSearch } from "../../../../types/search"; @@ -21,7 +21,9 @@ export class MangaReader { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const $ = load(data); - const rangeResult: number[] = $("div.volume-list-ul div.manga_list div.manga_list-wrap") + const rangeResult: number[] = $( + "div.volume-list-ul div.manga_list div.manga_list-wrap", + ) .find("div.item") .map((_, element) => { const mangaVolumeTitle = $(element) @@ -29,7 +31,8 @@ export class MangaReader { .text() .trim(); return Number(mangaVolumeTitle.split(" ").at(-1)); - }).get(); + }) + .get(); return rangeResult; } @@ -37,13 +40,15 @@ export class MangaReader { private async GetSpecificMangaChapterName( mangaId: number, chapterNumber: number, - language: typeof MangaReaderFilterLanguage[number], - type: MangaReaderChapterType + language: (typeof MangaReaderFilterLanguage)[number], + type: MangaReaderChapterType, ): Promise { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const $ = load(data); - let langCode: typeof MangaReaderFilterLanguage[number] = MangaReaderFilterLanguage[MangaReaderFilterLanguage.indexOf(language)] || ""; + let langCode: (typeof MangaReaderFilterLanguage)[number] = + MangaReaderFilterLanguage[MangaReaderFilterLanguage.indexOf(language)] || + ""; let result = ``; let chapterItemHtmlTag = ``; @@ -64,7 +69,10 @@ export class MangaReader { if (!chapters.length) throw new Error("Chapters doesn't found."); - const chaptersTitle: string[] = chapters.find(chapterTitleHtmlTag).map((_, element) => $(element).text().trim()).get(); + const chaptersTitle: string[] = chapters + .find(chapterTitleHtmlTag) + .map((_, element) => $(element).text().trim()) + .get(); for (let title of chaptersTitle) { if (title.includes(chapterTitleMatch)) { @@ -82,11 +90,17 @@ export class MangaReader { if (type === "chapter") idType = "chap"; else if (type === "volume") idType = "vol"; - const { data: pagesAjaxData } = await axios.get(`${this.url}/ajax/image/list/${idType}/${chapterId}?mode=horizontal&quality=high`); + const { data: pagesAjaxData } = await axios.get( + `${this.url}/ajax/image/list/${idType}/${chapterId}?mode=horizontal&quality=high`, + ); const $pagesAjaxData = load(pagesAjaxData.html); - const pagesSection = $pagesAjaxData("div#main-wrapper div.container-reader-hoz div#divslide div.divslide-wrapper div.ds-item").find("div.ds-image") + const pagesSection = $pagesAjaxData( + "div#main-wrapper div.container-reader-hoz div#divslide div.divslide-wrapper div.ds-item", + ).find("div.ds-image"); - let pages = pagesSection.map((_, element) => $pagesAjaxData(element).attr("data-url")).get(); + let pages = pagesSection + .map((_, element) => $pagesAjaxData(element).attr("data-url")) + .get(); return pages; } @@ -94,17 +108,23 @@ export class MangaReader { async GetMangaInfo(mangaId: number): Promise { try { const { data } = await axios.get(`${this.url}/a-${mangaId}`); - const { data: charactersAjaxList } = await axios.get(`${this.url}/ajax/character/list/${mangaId}`); + const { data: charactersAjaxList } = await axios.get( + `${this.url}/ajax/character/list/${mangaId}`, + ); const $ = load(data); const $characterListAjaxResult = load(charactersAjaxList.html); - const charactersSection = $characterListAjaxResult("div.character-list div.cl-item div.cli-info"); + const charactersSection = $characterListAjaxResult( + "div.character-list div.cl-item div.cli-info", + ); const title = $("h2.manga-name").text().trim(); - const altTitle = $("div.manga-name-or").text().trim() ? Array.of($("div.manga-name-or").text().trim()) : null; + const altTitle = $("div.manga-name-or").text().trim() + ? Array.of($("div.manga-name-or").text().trim()) + : null; const thumbnailUrl = $("div.manga-poster img.manga-poster-img").attr( - "src" + "src", ); const description = $("div.description").text().trim(); const status = $("div.anisc-info div.item") @@ -114,7 +134,10 @@ export class MangaReader { .trim(); // Manga genres - const mangaGenres: Array = $("div.genres").find("a").map((_, element) => $(element).text().trim()).get(); + const mangaGenres: Array = $("div.genres") + .find("a") + .map((_, element) => $(element).text().trim()) + .get(); const manga = new Manga(); @@ -130,43 +153,50 @@ export class MangaReader { manga.genres = mangaGenres; if (charactersSection.html()) { - const characters = charactersSection.find("h4.cl-name a").map((_, element) => $characterListAjaxResult(element).text().trim()).get(); + const characters = charactersSection + .find("h4.cl-name a") + .map((_, element) => $characterListAjaxResult(element).text().trim()) + .get(); manga.characters = characters; } else manga.characters = null; // Get manga chapters manga.chapters = []; - const mangaChapterItemSection = $( - "div.chapters-list-ul ul.ulclear" - ); + const mangaChapterItemSection = $("div.chapters-list-ul ul.ulclear"); let langCode: string = ``; if (mangaChapterItemSection?.first().attr("id")) langCode = mangaChapterItemSection.first().attr("id").split("-")[0]; - mangaChapterItemSection.first().find("li.chapter-item").each((_, element) => { - const mangaChapter = new MangaChapter(); - - const mangaTitle = $(element) - .find("a.item-link span.name") - .text() - .trim(); - const mangaChapterNumber = mangaTitle.split(" ").at(1).replace(":", ""); - - mangaChapter.title = mangaTitle; - mangaChapter.id = mangaId.toString(); - mangaChapter.url = `/manga/mangareader/chapter/${mangaId.toString()}?number=${mangaChapterNumber}&lang=${langCode}`; - mangaChapter.images = null; - - manga.chapters.push(mangaChapter); - }); + mangaChapterItemSection + .first() + .find("li.chapter-item") + .each((_, element) => { + const mangaChapter = new MangaChapter(); + + const mangaTitle = $(element) + .find("a.item-link span.name") + .text() + .trim(); + const mangaChapterNumber = mangaTitle + .split(" ") + .at(1) + .replace(":", ""); + + mangaChapter.title = mangaTitle; + mangaChapter.id = mangaId.toString(); + mangaChapter.url = `/manga/mangareader/chapter/${mangaId.toString()}?number=${mangaChapterNumber}&lang=${langCode}`; + mangaChapter.images = null; + + manga.chapters.push(mangaChapter); + }); // Get manga volumes const mangaVolumeRange = await this.GetMangaVolumeRange(mangaId); manga.volumes = []; const mangaVolumeItemSection = $( - "div.volume-list-ul div.manga_list div.manga_list-wrap" + "div.volume-list-ul div.manga_list div.manga_list-wrap", ); let langVolumeCode: string = ``; @@ -177,30 +207,33 @@ export class MangaReader { .attr("id") .split("-")[0]; - mangaVolumeItemSection.first().find("div.item").each((_, element) => { - const mangaVolume = new MangaVolume(); - - const mangaVolumeTitle = $(element) - .find("div.manga-poster span.tick-item") - .text() - .trim(); - const mangaVolumeNumber = mangaVolumeTitle.split(" ").at(-1); - const mangaVolumeThumbnail = $(element) - .find("div.manga-poster img.manga-poster-img") - .attr("src"); - - mangaVolume.range = [mangaVolumeRange.at(-1), mangaVolumeRange.at(0)]; - mangaVolume.id = mangaId.toString(); - mangaVolume.title = mangaVolumeTitle; - mangaVolume.number = Number(mangaVolumeNumber); - mangaVolume.thumbnail = mangaVolumeThumbnail; - mangaVolume.url = `/manga/mangareader/volume/${mangaId.toString()}?number=${mangaVolumeNumber}&lang=${langVolumeCode}`; - - manga.volumes.push(mangaVolume); - }); + mangaVolumeItemSection + .first() + .find("div.item") + .each((_, element) => { + const mangaVolume = new MangaVolume(); + + const mangaVolumeTitle = $(element) + .find("div.manga-poster span.tick-item") + .text() + .trim(); + const mangaVolumeNumber = mangaVolumeTitle.split(" ").at(-1); + const mangaVolumeThumbnail = $(element) + .find("div.manga-poster img.manga-poster-img") + .attr("src"); + + mangaVolume.range = [mangaVolumeRange.at(-1), mangaVolumeRange.at(0)]; + mangaVolume.id = mangaId.toString(); + mangaVolume.title = mangaVolumeTitle; + mangaVolume.number = Number(mangaVolumeNumber); + mangaVolume.thumbnail = mangaVolumeThumbnail; + mangaVolume.url = `/manga/mangareader/volume/${mangaId.toString()}?number=${mangaVolumeNumber}&lang=${langVolumeCode}`; + + manga.volumes.push(mangaVolume); + }); if ( - mangaGenres.some(genre => genre === "Hentai" || genre === "Ecchi") === + mangaGenres.some((genre) => genre === "Hentai" || genre === "Ecchi") === true ) manga.isNSFW = true; @@ -210,13 +243,13 @@ export class MangaReader { } catch (error) { console.log(error); throw new Error( - "I've found an error while trying to get the manga info." + "I've found an error while trying to get the manga info.", ); } } async Filter( - options: MangaReaderFilterData + options: MangaReaderFilterData, ): Promise> { const { type, @@ -231,7 +264,7 @@ export class MangaReader { endMonth, endDay, sort, - numPage + numPage, } = options; if ( startYear <= 0 || @@ -258,8 +291,8 @@ export class MangaReader { em: endMonth ?? "", ed: endDay ?? "", sort: sort ?? "", - page: numPage ?? 1 - } + page: numPage ?? 1, + }, }); const $ = load(data); @@ -293,7 +326,7 @@ export class MangaReader { id: mangaResultsID, title: mangaResultsTitle, thumbnail: new Image(mangaResultsThumbnail), - url: `/manga/mangareader/title/${mangaResultsID}` + url: `/manga/mangareader/title/${mangaResultsID}`, }); }); @@ -303,14 +336,16 @@ export class MangaReader { async GetMangaChapters( mangaId: number, chapterNumber: number, - language: typeof MangaReaderFilterLanguage[number], - type: MangaReaderChapterType + language: (typeof MangaReaderFilterLanguage)[number], + type: MangaReaderChapterType, ) { try { - const { data } = await axios.get(`${this.url}/read/a-${mangaId}/${language}/${type}-${chapterNumber}`); + const { data } = await axios.get( + `${this.url}/read/a-${mangaId}/${language}/${type}-${chapterNumber}`, + ); const $ = load(data); - const chapterId = $('div#wrapper').attr('data-reading-id'); + const chapterId = $("div#wrapper").attr("data-reading-id"); if (!chapterId) throw new Error("Chapter pages doesn't found."); @@ -318,7 +353,7 @@ export class MangaReader { mangaId, chapterNumber, language, - type + type, ); if (type === "chapter") { @@ -349,7 +384,7 @@ export class MangaReader { } catch (error) { console.log(error); throw new Error( - `I've found an error while trying to get the manga ${type} pages.` + `I've found an error while trying to get the manga ${type} pages.`, ); } } diff --git a/src/scraper/sites/manga/MangaReader/MangaReaderTypes.ts b/src/scraper/sites/manga/MangaReader/MangaReaderTypes.ts index 9115c54d..8fd53b8f 100644 --- a/src/scraper/sites/manga/MangaReader/MangaReaderTypes.ts +++ b/src/scraper/sites/manga/MangaReader/MangaReaderTypes.ts @@ -6,7 +6,7 @@ export enum MangaReaderFilterType { LightNovel, Manhwa, Manhua, - Comic = 8 + Comic = 8, } export enum MangaReaderFilterStatus { @@ -15,7 +15,7 @@ export enum MangaReaderFilterStatus { Publishing, OnHiatus, Discontinued, - NotYetPublished + NotYetPublished, } export enum MangaReaderFilterRatingType { @@ -25,7 +25,7 @@ export enum MangaReaderFilterRatingType { Teens, Mature, MildNudity, - Adults + Adults, } export enum MangaReaderFilterScore { @@ -39,10 +39,17 @@ export enum MangaReaderFilterScore { Good, VeryGood, Great, - Masterpiece + Masterpiece, } -export const MangaReaderFilterLanguage = ["", "en", "ja", "ko", "zh", "fr"] as const; +export const MangaReaderFilterLanguage = [ + "", + "en", + "ja", + "ko", + "zh", + "fr", +] as const; export enum MangaReaderFilterSort { All = "", @@ -50,7 +57,7 @@ export enum MangaReaderFilterSort { Score = "score", NameAZ = "name-az", ReleaseDate = "release-date", - MostViewed = "most-viewed" + MostViewed = "most-viewed", } export type MangaReaderChapterType = "chapter" | "volume"; @@ -60,7 +67,7 @@ export interface MangaReaderFilterData { status?: MangaReaderFilterStatus; ratingType?: MangaReaderFilterRatingType; score?: MangaReaderFilterScore; - language?: typeof MangaReaderFilterLanguage[number]; + language?: (typeof MangaReaderFilterLanguage)[number]; startYear?: number; startMonth?: number; startDay?: number; diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index fc3fdda9..fca0ad64 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -4,193 +4,236 @@ import { Manga, MangaChapter, IMangaResult } from "../../../../types/manga"; import { IResultSearch } from "../../../../types/search"; //Default Set Axios Cookie -axios.defaults.withCredentials = true -axios.defaults.headers.common["User-Agent"] = "Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-G532G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/12.0 Chrome/79.0.3945.136 Mobile Safari/537.36"; +axios.defaults.withCredentials = true; +axios.defaults.headers.common["User-Agent"] = + "Mozilla/5.0 (Linux; Android 6.0.1; SAMSUNG SM-G532G) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/12.0 Chrome/79.0.3945.136 Mobile Safari/537.36"; /** List of Domains * https://comick.app - * + * * https://api.comick.app - * + * * https://api.comick.io - * + * * https://comick.cc - * + * * https://meo.comick.pictures -*/ - + */ export class Comick { - readonly url = "https://comick.app"; - readonly api = "https://api.comick.io" - - async GetMangaByFilter(search?: string, type?: number, year?: string, genre?: string) { - try { - const { data } = await axios.get(`${this.api}/v1.0/search`, { - params: { - q: search, - status: type, - year: year, - genre: genre, - } - }); - - const ResultList: IResultSearch = { - results: [] - } - data.map((e: { id: number; title: string; md_covers: { b2key: string; }[]; slug: string; }) => { - const ListMangaResult: IMangaResult = { - id: e.id, - title: e.title, - thumbnail: { - url: "https://meo.comick.pictures/" + e.md_covers[0].b2key - }, - url: `/manga/comick/title/${e.slug}` - } - ResultList.results.push(ListMangaResult) - }) - - return ResultList - } catch (error) { - console.log(error) - } + readonly url = "https://comick.app"; + readonly api = "https://api.comick.io"; + + async GetMangaByFilter( + search?: string, + type?: number, + year?: string, + genre?: string, + ) { + try { + const { data } = await axios.get(`${this.api}/v1.0/search`, { + params: { + q: search, + status: type, + year: year, + genre: genre, + }, + }); + + const ResultList: IResultSearch = { + results: [], + }; + data.map( + (e: { + id: number; + title: string; + md_covers: { b2key: string }[]; + slug: string; + }) => { + const ListMangaResult: IMangaResult = { + id: e.id, + title: e.title, + thumbnail: { + url: "https://meo.comick.pictures/" + e.md_covers[0].b2key, + }, + url: `/manga/comick/title/${e.slug}`, + }; + ResultList.results.push(ListMangaResult); + }, + ); + + return ResultList; + } catch (error) { + console.log(error); } - - async GetMangaInfo(manga: string, lang: string): Promise { - try { - const { data } = await axios.get(`${this.api}/comic/${manga}`); - // build static - ///_next/data/S1XqseNRmzozm3TaUH1lU/comic/00-solo-leveling.json - const currentLang = lang ? `?lang=${lang}` : `?lang=en` - const mangaInfoParseObj = data - - const dataApi = await axios.get(`${this.api}/comic/${mangaInfoParseObj.comic.hid}/chapters${currentLang}`); - - const MangaInfo: Manga = { - id: mangaInfoParseObj.comic.id, - title: mangaInfoParseObj.comic.title, - altTitles: mangaInfoParseObj.comic.md_titles.map((e: { title: string; }) => e.title), - url: `/manga/comick/title/${mangaInfoParseObj.comic.slug}`, - description: mangaInfoParseObj.comic.desc, - isNSFW: mangaInfoParseObj.comic.hentai, - langlist: mangaInfoParseObj.langList, - status: mangaInfoParseObj.comic.status == "1" ? "ongoing" : "completed", - authors: mangaInfoParseObj.authors.map((e: { name: string; }) => e.name), - genres: mangaInfoParseObj.comic.md_comic_md_genres.map((e: { md_genres: {name:string;} }) => e.md_genres.name), - chapters: [], - thumbnail: { - url: "https://meo.comick.pictures/" + mangaInfoParseObj.comic.md_covers[0].b2key - } - } - - dataApi.data.chapters.map((e: { id: number; title: string; hid: string; chap: number; created_at: string; lang: string; }) => { - const mindate = new Date(e.created_at); - const langChapter = currentLang ? currentLang : "?lang=" + e.lang - - const MangaInfoChapter: MangaChapter = { - id: e.id, - title: e.title, - url: `/manga/comick/chapter/${e.hid}-${mangaInfoParseObj.comic.slug}-${e.chap ? e.chap : "err"}${langChapter}`, - number: e.chap, - images: null, - cover: null, - date: { - year: mindate.getFullYear() ? mindate.getFullYear() : null, - month: mindate.getMonth() ? mindate.getMonth() : null, - day: mindate.getDay() ? mindate.getDay() : null - } - } - return MangaInfo.chapters.push(!langChapter.includes("?lang=id") ? MangaInfoChapter : null) - }) - - return MangaInfo - } catch (error) { - console.log(error) - } + } + + async GetMangaInfo(manga: string, lang: string): Promise { + try { + const { data } = await axios.get(`${this.api}/comic/${manga}`); + // build static + ///_next/data/S1XqseNRmzozm3TaUH1lU/comic/00-solo-leveling.json + const currentLang = lang ? `?lang=${lang}` : `?lang=en`; + const mangaInfoParseObj = data; + + const dataApi = await axios.get( + `${this.api}/comic/${mangaInfoParseObj.comic.hid}/chapters${currentLang}`, + ); + + const MangaInfo: Manga = { + id: mangaInfoParseObj.comic.id, + title: mangaInfoParseObj.comic.title, + altTitles: mangaInfoParseObj.comic.md_titles.map( + (e: { title: string }) => e.title, + ), + url: `/manga/comick/title/${mangaInfoParseObj.comic.slug}`, + description: mangaInfoParseObj.comic.desc, + isNSFW: mangaInfoParseObj.comic.hentai, + langlist: mangaInfoParseObj.langList, + status: mangaInfoParseObj.comic.status == "1" ? "ongoing" : "completed", + authors: mangaInfoParseObj.authors.map((e: { name: string }) => e.name), + genres: mangaInfoParseObj.comic.md_comic_md_genres.map( + (e: { md_genres: { name: string } }) => e.md_genres.name, + ), + chapters: [], + thumbnail: { + url: + "https://meo.comick.pictures/" + + mangaInfoParseObj.comic.md_covers[0].b2key, + }, + }; + + dataApi.data.chapters.map( + (e: { + id: number; + title: string; + hid: string; + chap: number; + created_at: string; + lang: string; + }) => { + const mindate = new Date(e.created_at); + const langChapter = currentLang ? currentLang : "?lang=" + e.lang; + + const MangaInfoChapter: MangaChapter = { + id: e.id, + title: e.title, + url: `/manga/comick/chapter/${e.hid}-${ + mangaInfoParseObj.comic.slug + }-${e.chap ? e.chap : "err"}${langChapter}`, + number: e.chap, + images: null, + cover: null, + date: { + year: mindate.getFullYear() ? mindate.getFullYear() : null, + month: mindate.getMonth() ? mindate.getMonth() : null, + day: mindate.getDay() ? mindate.getDay() : null, + }, + }; + return MangaInfo.chapters.push( + !langChapter.includes("?lang=id") ? MangaInfoChapter : null, + ); + }, + ); + + return MangaInfo; + } catch (error) { + console.log(error); } - - async GetChapterInfo(manga: string, lang: string) { - try { - - const currentLang = lang ? "-" + lang : "-en"; - const hid = manga.substring(0, manga.indexOf("-")); - const idTitle = manga.substring(manga.indexOf("-") + 1); - const idNumber = idTitle.substring(idTitle.lastIndexOf("-") + 1); - const title = idTitle.substring(0, idTitle.lastIndexOf("-")); - - let urlchange = "" - - if (idNumber != "err") { - urlchange = `${hid}-chapter-${idNumber}${currentLang}` - } else { - urlchange = hid - } - - const { data } = await axios.get(`${this.url}/comic/${title}/${urlchange}`); - const $ = cheerio.load(data); - - if (JSON.parse($("#__NEXT_DATA__").html()).isFallback == false) { - const mangaChapterInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props.pageProps - const mindate = new Date(mangaChapterInfoParseObj.chapter.created_at); - - const MangaChapterInfoChapter: MangaChapter = { - id: mangaChapterInfoParseObj.chapter.id, - title: mangaChapterInfoParseObj.seoTitle, - url: `/manga/comick/chapter/${manga}`, - number: mangaChapterInfoParseObj.chapter.chap, - images: mangaChapterInfoParseObj.chapter.md_images.map((e: { w: number; h: number; name: string; b2key: string; }) => { - return { - width: e.w, - height: e.h, - name: e.name, - image: "https://meo.comick.pictures/" + e.b2key - } - }), - cover: "https://meo.comick.pictures/" + mangaChapterInfoParseObj.chapter.md_comics.md_covers[0].b2key, - date: { - year: mindate.getFullYear() ? mindate.getFullYear() : null, - month: mindate.getMonth() ? mindate.getMonth() : null, - day: mindate.getDay() ? mindate.getDay() : null - } - } - return MangaChapterInfoChapter; - - } else { - const buildid = JSON.parse($("#__NEXT_DATA__").html()).buildId - const currentUrl = idNumber == "err" ? `${title}/${hid}.json?slug=${title}&chapter=${hid}` : `${title}/${hid}-chapter-${idNumber}${currentLang}.json?slug=${title}&chapter=${hid}-chapter-${idNumber}${currentLang}` - const dataBuild = await axios.get(`${this.url}/_next/data/${buildid}/comic/${currentUrl}`); - - const mindate = new Date(dataBuild.data.pageProps.chapter.created_at); - - const MangaChapterInfoChapter: MangaChapter = { - id: dataBuild.data.pageProps.chapter.id, - title: dataBuild.data.pageProps.seoTitle, - url: `/manga/comick/chapter/${manga}`, - number: dataBuild.data.pageProps.chapter.chap, - images: dataBuild.data.pageProps.chapter.md_images.map((s: { w: number; h: number; name: string; b2key: string; }) => { - return { - width: s.w, - height: s.h, - name: s.name, - image: "https://meo.comick.pictures/" + s.b2key - } - }), - cover: "https://meo.comick.pictures/" + dataBuild.data.pageProps.chapter.md_comics.md_covers[0].b2key, - date: { - year: mindate.getFullYear() ? mindate.getFullYear() : null, - month: mindate.getMonth() ? mindate.getMonth() : null, - day: mindate.getDay() ? mindate.getDay() : null - } - } - - return MangaChapterInfoChapter; - - } - } catch (error) { - console.log(error) - } + } + + async GetChapterInfo(manga: string, lang: string) { + try { + const currentLang = lang ? "-" + lang : "-en"; + const hid = manga.substring(0, manga.indexOf("-")); + const idTitle = manga.substring(manga.indexOf("-") + 1); + const idNumber = idTitle.substring(idTitle.lastIndexOf("-") + 1); + const title = idTitle.substring(0, idTitle.lastIndexOf("-")); + + let urlchange = ""; + + if (idNumber != "err") { + urlchange = `${hid}-chapter-${idNumber}${currentLang}`; + } else { + urlchange = hid; + } + + const { data } = await axios.get( + `${this.url}/comic/${title}/${urlchange}`, + ); + const $ = cheerio.load(data); + + if (JSON.parse($("#__NEXT_DATA__").html()).isFallback == false) { + const mangaChapterInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()) + .props.pageProps; + const mindate = new Date(mangaChapterInfoParseObj.chapter.created_at); + + const MangaChapterInfoChapter: MangaChapter = { + id: mangaChapterInfoParseObj.chapter.id, + title: mangaChapterInfoParseObj.seoTitle, + url: `/manga/comick/chapter/${manga}`, + number: mangaChapterInfoParseObj.chapter.chap, + images: mangaChapterInfoParseObj.chapter.md_images.map( + (e: { w: number; h: number; name: string; b2key: string }) => { + return { + width: e.w, + height: e.h, + name: e.name, + image: "https://meo.comick.pictures/" + e.b2key, + }; + }, + ), + cover: + "https://meo.comick.pictures/" + + mangaChapterInfoParseObj.chapter.md_comics.md_covers[0].b2key, + date: { + year: mindate.getFullYear() ? mindate.getFullYear() : null, + month: mindate.getMonth() ? mindate.getMonth() : null, + day: mindate.getDay() ? mindate.getDay() : null, + }, + }; + return MangaChapterInfoChapter; + } else { + const buildid = JSON.parse($("#__NEXT_DATA__").html()).buildId; + const currentUrl = + idNumber == "err" + ? `${title}/${hid}.json?slug=${title}&chapter=${hid}` + : `${title}/${hid}-chapter-${idNumber}${currentLang}.json?slug=${title}&chapter=${hid}-chapter-${idNumber}${currentLang}`; + const dataBuild = await axios.get( + `${this.url}/_next/data/${buildid}/comic/${currentUrl}`, + ); + + const mindate = new Date(dataBuild.data.pageProps.chapter.created_at); + + const MangaChapterInfoChapter: MangaChapter = { + id: dataBuild.data.pageProps.chapter.id, + title: dataBuild.data.pageProps.seoTitle, + url: `/manga/comick/chapter/${manga}`, + number: dataBuild.data.pageProps.chapter.chap, + images: dataBuild.data.pageProps.chapter.md_images.map( + (s: { w: number; h: number; name: string; b2key: string }) => { + return { + width: s.w, + height: s.h, + name: s.name, + image: "https://meo.comick.pictures/" + s.b2key, + }; + }, + ), + cover: + "https://meo.comick.pictures/" + + dataBuild.data.pageProps.chapter.md_comics.md_covers[0].b2key, + date: { + year: mindate.getFullYear() ? mindate.getFullYear() : null, + month: mindate.getMonth() ? mindate.getMonth() : null, + day: mindate.getDay() ? mindate.getDay() : null, + }, + }; + + return MangaChapterInfoChapter; + } + } catch (error) { + console.log(error); } - + } } - - diff --git a/src/scraper/sites/manga/inmanga/Inmanga.ts b/src/scraper/sites/manga/inmanga/Inmanga.ts index dcaad05a..f78ba6de 100644 --- a/src/scraper/sites/manga/inmanga/Inmanga.ts +++ b/src/scraper/sites/manga/inmanga/Inmanga.ts @@ -1,162 +1,252 @@ import * as cheerio from "cheerio"; import axios from "axios"; -import { Manga, MangaChapter, IMangaResult } from "../../../../types/manga" +import { Manga, MangaChapter, IMangaResult } from "../../../../types/manga"; import { IResultSearch } from "../../../../types/search"; //Default Set Axios Cookie -axios.defaults.withCredentials = true -axios.defaults.headers.common["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"; +axios.defaults.withCredentials = true; +axios.defaults.headers.common["User-Agent"] = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"; export class Inmanga { - readonly url = "https://inmanga.com"; - - async GetMangaByFilter(search?: string, type?: number, genre?: string[]) { - try { - const formdata = new FormData(); - formdata.append("filter[queryString]", search); - formdata.append("filter[broadcastStatus]", String(type)) - formdata.append("filter[skip]", "0"); - formdata.append("filter[take]", "10"); - const genreList = ['33', '34', '35', '36', '37', '38', '39', '40', '41', '42', '43', '44', '45', '46', '47', '48', '49', '50', '51', '52', '53', '54', '55', '56', '57', '58', '59', '60', '61', '62', '63', '64', '65', '66', '67', '68', '69', '70', '71', '72', '73', '74', '75', '76', '77', '78', '79', '80', '81', '82', '83', '84', '85', '86', '87', '88', '-1'] - - if (genre) { - genre.map((e) => { - if (genreList.includes(e)) { - formdata.append("filter[generes][]", genreList[genreList.indexOf(e)]); - } - }) - } else { - formdata.append("filter[generes][]", "-1"); - } - - const bodyContent = formdata; - const { data } = await axios.post(`${this.url}/manga/getMangasConsultResult`, bodyContent); - const $ = cheerio.load(data); - - const ResultList: IResultSearch = { - results: [] - } - - $("a").each((_i, e) => { - const idtd = $(e).attr("href").split("/") - const name = idtd[3] - const cid = idtd[4] - const title = $(e).find(".list-group.col-xs-12 .m0.list-group-item.ellipsed-text").text().trim() - - const ListMangaResult: IMangaResult = { - id: null, - title: title, - thumbnail: { - url: `https://inmanga.com/thumbnails/manga/${name}/${cid}` - }, - - // old version `/manga/inmanga/title/${title.replace(/[^a-zA-Z:]/g, "-")}` - url: `/manga/inmanga/title/${name}?cid=${cid}` - } - ResultList.results.push(ListMangaResult) - }) - - return ResultList - } catch (error) { - console.log(error) - } + readonly url = "https://inmanga.com"; + + async GetMangaByFilter(search?: string, type?: number, genre?: string[]) { + try { + const formdata = new FormData(); + formdata.append("filter[queryString]", search); + formdata.append("filter[broadcastStatus]", String(type)); + formdata.append("filter[skip]", "0"); + formdata.append("filter[take]", "10"); + const genreList = [ + "33", + "34", + "35", + "36", + "37", + "38", + "39", + "40", + "41", + "42", + "43", + "44", + "45", + "46", + "47", + "48", + "49", + "50", + "51", + "52", + "53", + "54", + "55", + "56", + "57", + "58", + "59", + "60", + "61", + "62", + "63", + "64", + "65", + "66", + "67", + "68", + "69", + "70", + "71", + "72", + "73", + "74", + "75", + "76", + "77", + "78", + "79", + "80", + "81", + "82", + "83", + "84", + "85", + "86", + "87", + "88", + "-1", + ]; + + if (genre) { + genre.map((e) => { + if (genreList.includes(e)) { + formdata.append( + "filter[generes][]", + genreList[genreList.indexOf(e)], + ); + } + }); + } else { + formdata.append("filter[generes][]", "-1"); + } + + const bodyContent = formdata; + const { data } = await axios.post( + `${this.url}/manga/getMangasConsultResult`, + bodyContent, + ); + const $ = cheerio.load(data); + + const ResultList: IResultSearch = { + results: [], + }; + + $("a").each((_i, e) => { + const idtd = $(e).attr("href").split("/"); + const name = idtd[3]; + const cid = idtd[4]; + const title = $(e) + .find(".list-group.col-xs-12 .m0.list-group-item.ellipsed-text") + .text() + .trim(); + + const ListMangaResult: IMangaResult = { + id: null, + title: title, + thumbnail: { + url: `https://inmanga.com/thumbnails/manga/${name}/${cid}`, + }, + + // old version `/manga/inmanga/title/${title.replace(/[^a-zA-Z:]/g, "-")}` + url: `/manga/inmanga/title/${name}?cid=${cid}`, + }; + ResultList.results.push(ListMangaResult); + }); + + return ResultList; + } catch (error) { + console.log(error); } - - async GetMangaInfo(manga: string,cid: string): Promise { - try { - const dataPost = await axios.get(`${this.url}/ver/manga/${manga}/${cid}`); - const $_ = cheerio.load(dataPost.data); - - const MangaInfo: Manga = { - id: cid, - title: $_("div.col-md-3.col-sm-4 div.panel-heading.visible-xs").text(), - altTitles: [], - url: `/manga/inmanga/title/${manga}`, - description: $_("body > div > section > div > div > div:nth-child(6) > div > div.panel-body").text().trim(), - isNSFW: false, - status: $_(".col-md-3.col-sm-4 .list-group > a:nth-child(1) > span").text() == "En emisión" ? "ongoing" : "completed", - authors: [], - genres: [], - chapters: [], - thumbnail: { - url: `https://inmanga.com/thumbnails/manga/${manga}/${cid}` - } - } - - $_(".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .text-muted span").each((_i, e) => MangaInfo.altTitles.push($_(e).text().replace(";", ""))) - $_(".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm").each((_i, e) => MangaInfo.genres.push($_(e).text().trim())) - - MangaInfo.altTitles.slice(MangaInfo.altTitles.indexOf('""'), 0) - MangaInfo.genres.slice(MangaInfo.genres.indexOf('""'), 0) - - const dataChPost = await axios.get(`${this.url}/chapter/getall?mangaIdentification=${cid}`); - const dataCh = JSON.parse(dataChPost.data.data); - dataCh.result.map((e: { Id: number; MangaName: string; Number: number; Identification: string; }) => { - const MangaInfoChapter: MangaChapter = { - id: e.Id, - title: e.MangaName, - url: `/manga/inmanga/chapter/${manga}-${e.Number}?cid=${e.Identification}`, // Change url (: = title ) manga.replace(/[^a-zA-Z:]/g," ") - number: e.Number, - images: null, - cover: null, - date: { - year: null, - month: null, - day: null - } - } - MangaInfo.chapters.push(MangaInfoChapter); - }) - - return MangaInfo - } catch (error) { - console.log(error) - } + } + + async GetMangaInfo(manga: string, cid: string): Promise { + try { + const dataPost = await axios.get(`${this.url}/ver/manga/${manga}/${cid}`); + const $_ = cheerio.load(dataPost.data); + + const MangaInfo: Manga = { + id: cid, + title: $_("div.col-md-3.col-sm-4 div.panel-heading.visible-xs").text(), + altTitles: [], + url: `/manga/inmanga/title/${manga}`, + description: $_( + "body > div > section > div > div > div:nth-child(6) > div > div.panel-body", + ) + .text() + .trim(), + isNSFW: false, + status: + $_(".col-md-3.col-sm-4 .list-group > a:nth-child(1) > span").text() == + "En emisión" + ? "ongoing" + : "completed", + authors: [], + genres: [], + chapters: [], + thumbnail: { + url: `https://inmanga.com/thumbnails/manga/${manga}/${cid}`, + }, + }; + + $_( + ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .text-muted span", + ).each((_i, e) => + MangaInfo.altTitles.push($_(e).text().replace(";", "")), + ); + $_( + ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm", + ).each((_i, e) => MangaInfo.genres.push($_(e).text().trim())); + + MangaInfo.altTitles.slice(MangaInfo.altTitles.indexOf('""'), 0); + MangaInfo.genres.slice(MangaInfo.genres.indexOf('""'), 0); + + const dataChPost = await axios.get( + `${this.url}/chapter/getall?mangaIdentification=${cid}`, + ); + const dataCh = JSON.parse(dataChPost.data.data); + dataCh.result.map( + (e: { + Id: number; + MangaName: string; + Number: number; + Identification: string; + }) => { + const MangaInfoChapter: MangaChapter = { + id: e.Id, + title: e.MangaName, + url: `/manga/inmanga/chapter/${manga}-${e.Number}?cid=${e.Identification}`, // Change url (: = title ) manga.replace(/[^a-zA-Z:]/g," ") + number: e.Number, + images: null, + cover: null, + date: { + year: null, + month: null, + day: null, + }, + }; + MangaInfo.chapters.push(MangaInfoChapter); + }, + ); + + return MangaInfo; + } catch (error) { + console.log(error); } - - async GetChapterInfo(manga: string, cid: string) { - try { - const title = manga.substring(0, manga.lastIndexOf("-")); - const idNumber = Number(manga.substring(manga.lastIndexOf("-") + 1)); - - const { data } = await axios.get(`${this.url}/chapter/chapterIndexControls?identification=${cid}`) - const $ = cheerio.load(data); - - const allimages = [] - - const MangaChapterInfoChapter: MangaChapter = { - id: 1, - title: "", - url: `/manga/inmanga/chapter/`, - number: idNumber, - images: allimages, - cover: null, - date: { - year: null, - month: null, - day: null - } - } - - $(".p0.col-sm-12.col-xs-12.PagesContainer a").each((_i, e) => { - const id = $(e).find("img").attr("id") - const alt = $(e).find("img").attr("alt") - const page = $(e).find("img").attr("data-pagenumber") - - allimages.push({ - width: "", - height: "", - name: alt, - url: `https://pack-yak.intomanga.com/images/manga/${title}/chapter/${idNumber}/page/${page}/${id}` - }) - }) - - return MangaChapterInfoChapter; - } catch (error) { - console.log(error) - } + } + + async GetChapterInfo(manga: string, cid: string) { + try { + const title = manga.substring(0, manga.lastIndexOf("-")); + const idNumber = Number(manga.substring(manga.lastIndexOf("-") + 1)); + + const { data } = await axios.get( + `${this.url}/chapter/chapterIndexControls?identification=${cid}`, + ); + const $ = cheerio.load(data); + + const allimages = []; + + const MangaChapterInfoChapter: MangaChapter = { + id: 1, + title: "", + url: `/manga/inmanga/chapter/`, + number: idNumber, + images: allimages, + cover: null, + date: { + year: null, + month: null, + day: null, + }, + }; + + $(".p0.col-sm-12.col-xs-12.PagesContainer a").each((_i, e) => { + const id = $(e).find("img").attr("id"); + const alt = $(e).find("img").attr("alt"); + const page = $(e).find("img").attr("data-pagenumber"); + + allimages.push({ + width: "", + height: "", + name: alt, + url: `https://pack-yak.intomanga.com/images/manga/${title}/chapter/${idNumber}/page/${page}/${id}`, + }); + }); + + return MangaChapterInfoChapter; + } catch (error) { + console.log(error); } - + } } - - diff --git a/src/scraper/sites/manga/manganelo/Manganelo.ts b/src/scraper/sites/manga/manganelo/Manganelo.ts index 99c6b919..8a59f1ff 100644 --- a/src/scraper/sites/manga/manganelo/Manganelo.ts +++ b/src/scraper/sites/manga/manganelo/Manganelo.ts @@ -24,96 +24,131 @@ export class Manganelo { } private GetMangaStatus(data: cheerio.Root) { - const selector = data("div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(3) > td.table-value"); + const selector = data( + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(3) > td.table-value", + ); - if (selector.length == 0) - return null; + if (selector.length == 0) return null; - if (selector.text().trim() == "Ongoing") - return "ongoing"; - else - return "completed"; + if (selector.text().trim() == "Ongoing") return "ongoing"; + else return "completed"; } private GetMangaAuthors(data: cheerio.Root): string[] | null { - const selector = data("div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(2) > td.table-value"); - - if (selector.length == 0 && selector.find("a.a-h").length == 0) - return null; - - return selector.find("a.a-h").map((_, element) => { - return data(element).text().trim(); - }).get(); + const selector = data( + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(2) > td.table-value", + ); + + if (selector.length == 0 && selector.find("a.a-h").length == 0) return null; + + return selector + .find("a.a-h") + .map((_, element) => { + return data(element).text().trim(); + }) + .get(); } private GetMangaGenres(data: cheerio.Root): string[] | null { - const selector = data("div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(4) > td.table-value"); - - if (selector.length == 0 && selector.find("a.a-h").length == 0) - return null; - - return selector.find("a.a-h").map((_, element) => { - return data(element).text().trim(); - }).get(); + const selector = data( + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(4) > td.table-value", + ); + + if (selector.length == 0 && selector.find("a.a-h").length == 0) return null; + + return selector + .find("a.a-h") + .map((_, element) => { + return data(element).text().trim(); + }) + .get(); } private isNsfw(genres: string[]) { - return genres.some(genre => genre === "Pornographic" || genre === "Mature" || genre === "Erotica"); + return genres.some( + (genre) => + genre === "Pornographic" || genre === "Mature" || genre === "Erotica", + ); } private GetMangaPages(data: cheerio.Root) { - if (data("div.container-chapter-reader").length == 0 && data("div.container-chapter-reader > img").length == 0) + if ( + data("div.container-chapter-reader").length == 0 && + data("div.container-chapter-reader > img").length == 0 + ) return null; - return data("div.container-chapter-reader > img").map((_, element) => data(element).attr("data-src")).get(); + return data("div.container-chapter-reader > img") + .map((_, element) => data(element).attr("data-src")) + .get(); } private GetMangaSearchResults(data: cheerio.Root): IMangaResult[] | null { const section = data("div.panel-content-genres"); - if (section.length === 0) - return null; - - return section.find("div.content-genres-item").map((_, element) => { - const mangaResultId = data(element).find("a.genres-item-img").attr("href").split("-").at(-1); - const name = data(element).find("a.genres-item-img").attr("title").trim(); - - const mangaInfoResults: IMangaResult = { - id: mangaResultId, - title: name, - url: `/manga/${this.name}/title/${mangaResultId}` - } - - return mangaInfoResults; - }).get(); + if (section.length === 0) return null; + + return section + .find("div.content-genres-item") + .map((_, element) => { + const mangaResultId = data(element) + .find("a.genres-item-img") + .attr("href") + .split("-") + .at(-1); + const name = data(element) + .find("a.genres-item-img") + .attr("title") + .trim(); + + const mangaInfoResults: IMangaResult = { + id: mangaResultId, + title: name, + url: `/manga/${this.name}/title/${mangaResultId}`, + }; + + return mangaInfoResults; + }) + .get(); } async GetMangaInfo(mangaId: string) { const { data } = await axios.get(`${this.url}/manga/manga-${mangaId}`); const $ = load(data); - const manga = new Manga; + const manga = new Manga(); - const title = $("div.panel-story-info > div.story-info-right > h1").text().trim(); + const title = $("div.panel-story-info > div.story-info-right > h1") + .text() + .trim(); const description = this.GetMangaDescription($); - const thumbnail = this.url + $("div.panel-story-info > div.story-info-left > span.info-image > img").attr("src"); - const altTitle = $("table > tbody > tr:nth-child(1) > td.table-value > h2").text().trim(); + const thumbnail = + this.url + + $( + "div.panel-story-info > div.story-info-left > span.info-image > img", + ).attr("src"); + const altTitle = $("table > tbody > tr:nth-child(1) > td.table-value > h2") + .text() + .trim(); const status = this.GetMangaStatus($); const authors = this.GetMangaAuthors($); const genres = this.GetMangaGenres($); - const chapters = $("div.panel-story-chapter-list").find("ul > li.a-h").map((_, element) => { - const chapter = new MangaChapter; - const url = $(element).find("a.chapter-name").attr("href"); + const chapters = $("div.panel-story-chapter-list") + .find("ul > li.a-h") + .map((_, element) => { + const chapter = new MangaChapter(); + const url = $(element).find("a.chapter-name").attr("href"); - const chapterId = url.substring(url.lastIndexOf("-") + 1); + const chapterId = url.substring(url.lastIndexOf("-") + 1); - chapter.id = Number(chapterId); - chapter.title = $(element).find("a.chapter-name").text().trim(); - chapter.url = `/manga/${this.name}/chapter/${mangaId}?num=${chapterId}`; - chapter.number = Number(chapterId); - chapter.images = null; + chapter.id = Number(chapterId); + chapter.title = $(element).find("a.chapter-name").text().trim(); + chapter.url = `/manga/${this.name}/chapter/${mangaId}?num=${chapterId}`; + chapter.number = Number(chapterId); + chapter.images = null; - return chapter; - }).get(); + return chapter; + }) + .get(); manga.id = mangaId; manga.url = `/manga/${this.name}/title/${mangaId}`; @@ -145,12 +180,17 @@ export class Manganelo { } async GetMangaChapters(mangaId: string, chapterNumber: number) { - const { data } = await axios.get(`${this.url}/chapter/manga-${mangaId}/chapter-${chapterNumber}`); + const { data } = await axios.get( + `${this.url}/chapter/manga-${mangaId}/chapter-${chapterNumber}`, + ); const $ = load(data); const images = this.GetMangaPages($); - const name = $("body > div.body-site > div:nth-child(1) > div.panel-breadcrumb > a").eq(-1).attr("title") || null; - const chapter = new MangaChapter; + const name = + $("body > div.body-site > div:nth-child(1) > div.panel-breadcrumb > a") + .eq(-1) + .attr("title") || null; + const chapter = new MangaChapter(); chapter.id = Number(chapterNumber); chapter.title = name; diff --git a/src/scraper/sites/manga/nhentai/Nhentai.ts b/src/scraper/sites/manga/nhentai/Nhentai.ts index 2901e62e..7593b9b0 100644 --- a/src/scraper/sites/manga/nhentai/Nhentai.ts +++ b/src/scraper/sites/manga/nhentai/Nhentai.ts @@ -47,7 +47,7 @@ class NhentaiMangaInfo { async getMangaInfoById(mangaId: string) { try { const { data } = await axios.get(`https://nhentai.to/g/${mangaId}`); - + const $ = load(data); const manga = new Manga(); @@ -56,7 +56,6 @@ class NhentaiMangaInfo { manga.authors = []; manga.chapters = []; - manga.title = $("div#info h1").text(); manga.thumbnail = { url: $("div#cover a img").attr("src"), diff --git a/src/scraper/sites/manga/tmomanga/Page.js b/src/scraper/sites/manga/tmomanga/Page.js index ab1adb48..e1b72cb8 100644 --- a/src/scraper/sites/manga/tmomanga/Page.js +++ b/src/scraper/sites/manga/tmomanga/Page.js @@ -1,65 +1,84 @@ import axios from "axios"; import * as cheerio from "cheerio"; -import { Chapter, ChapterView, Manga } from "../../../../utils/manga/schemaProviders.js"; +import { + Chapter, + ChapterView, + Manga, +} from "../../../../utils/manga/schemaProviders.js"; import { Image } from "../../../../utils/schemaProviders.js"; export const TMOManga = { - url: 'https://tmomanga.com' + url: "https://tmomanga.com", }; /** - * - * @param {string} url + * + * @param {string} url * @returns {Chapter} */ async function getChapter(url) { - const $ = cheerio.load((await axios.get(url)).data); - let chapter = new Chapter(); + const $ = cheerio.load((await axios.get(url)).data); + let chapter = new Chapter(); - chapter.title = $('h1#chapter-heading').text().trim(); - chapter.url = url; - chapter.number = parseInt($('li.active').text().trim().split(' ').pop()); - chapter.images = $('div#images_chapter img').map((i, el) => { - if (i === 0) - chapter.cover = $(el).attr('data-src'); - return $(el).attr('data-src'); - }).toArray(); - return chapter; + chapter.title = $("h1#chapter-heading").text().trim(); + chapter.url = url; + chapter.number = parseInt($("li.active").text().trim().split(" ").pop()); + chapter.images = $("div#images_chapter img") + .map((i, el) => { + if (i === 0) chapter.cover = $(el).attr("data-src"); + return $(el).attr("data-src"); + }) + .toArray(); + return chapter; } /** - * - * @param {string} url + * + * @param {string} url * @returns {Manga} */ async function getManga(url) { - const $ = cheerio.load((await axios.get(url)).data); - const data = $('div.post-content-data div.post-content_item'); + const $ = cheerio.load((await axios.get(url)).data); + const data = $("div.post-content-data div.post-content_item"); - let manga = new Manga(); - manga.title = $('div.post-title h1').text(); - manga.url = url; - manga.image = new Image($('div.summary_image img').attr('src'), null); - manga.synopsis = $('div.description-summary p').text().trim(); - manga.year = $(data.get(0)).find('div.summary-content').text().trim(); - manga.genres = $(data.get(1)).find('a').map((i, el) => { return $(el).text().trim() }).toArray(); - manga.chapters = $('ul.sub-chap li').children().map((i, el) => { return $(el).attr('href') }).toArray(); - return manga; + let manga = new Manga(); + manga.title = $("div.post-title h1").text(); + manga.url = url; + manga.image = new Image($("div.summary_image img").attr("src"), null); + manga.synopsis = $("div.description-summary p").text().trim(); + manga.year = $(data.get(0)).find("div.summary-content").text().trim(); + manga.genres = $(data.get(1)) + .find("a") + .map((i, el) => { + return $(el).text().trim(); + }) + .toArray(); + manga.chapters = $("ul.sub-chap li") + .children() + .map((i, el) => { + return $(el).attr("href"); + }) + .toArray(); + return manga; } /** - * - * @param {*} element + * + * @param {*} element * @returns {ChapterView} */ function getChapterView(element) { - let view = new ChapterView(); - view.title = element.find('span.manga-title-updated').text().trim() + ' - ' + - element.find('span.manga-episode-title').text().trim(); - view.url = element.find('a').attr('href'); - view.image = element.find('img').attr('src'); - view.manga = view.url.substring(0, view.url.lastIndexOf('-')).replace('capitulo', 'manga'); - return view; + let view = new ChapterView(); + view.title = + element.find("span.manga-title-updated").text().trim() + + " - " + + element.find("span.manga-episode-title").text().trim(); + view.url = element.find("a").attr("href"); + view.image = element.find("img").attr("src"); + view.manga = view.url + .substring(0, view.url.lastIndexOf("-")) + .replace("capitulo", "manga"); + return view; } /** @@ -67,43 +86,47 @@ function getChapterView(element) { * @returns {(Manga[] | ChapterView[])} */ async function getSectionContent(number) { - try { - let content = []; - const $ = cheerio.load((await axios.get(TMOManga.url)).data); - const elements = $($('div.main-col-inner').get(number)).find('div.row').children(); - for (let i = 0; i < elements.length; i++) { - content.push(number == 1 ? await getManga($(elements[i]).find('h3 a').attr('href')) : - getChapterView($(elements[i]))); - } - return content; - } catch (error) { - console.log(error); + try { + let content = []; + const $ = cheerio.load((await axios.get(TMOManga.url)).data); + const elements = $($("div.main-col-inner").get(number)) + .find("div.row") + .children(); + for (let i = 0; i < elements.length; i++) { + content.push( + number == 1 + ? await getManga($(elements[i]).find("h3 a").attr("href")) + : getChapterView($(elements[i])), + ); } - return []; + return content; + } catch (error) { + console.log(error); + } + return []; } /** - * + * * @returns {Manga[]} */ async function getLastMangas() { - return await getSectionContent(1); + return await getSectionContent(1); } /** - * + * * @returns {ChapterView[]} */ async function getLastChapters() { - return await getSectionContent(0); + return await getSectionContent(0); } //console.log(await getChapter('https://tmomanga.com/capitulo/soredemo-ayumu-wa-yosetekuru-187.00')); -export default -{ - getLastMangas, - getLastChapters, - getManga, - getChapter -} \ No newline at end of file +export default { + getLastMangas, + getLastChapters, + getManga, + getChapter, +}; diff --git a/src/scraper/sites/manga/tmomanga/filter.js b/src/scraper/sites/manga/tmomanga/filter.js index fae4f6c7..9d82a4a1 100644 --- a/src/scraper/sites/manga/tmomanga/filter.js +++ b/src/scraper/sites/manga/tmomanga/filter.js @@ -3,39 +3,44 @@ import * as cheerio from "cheerio"; import utils from "../../../../utils/utilities.js"; /** - * - * @param {string} name + * + * @param {string} name * @param {string} genre * @return */ function getSearchURL(name, genre) { - if (utils.isUsableValue(genre)) - url = `https://tmomanga.com/genero/${genre.toLowerCase().replace(' ', '-')}`; - else if (utils.isUsableValue(name)) - url = `https://tmomanga.com/biblioteca?search=${encodeURI(name).split('%20').join('+')}`; - return null; + if (utils.isUsableValue(genre)) + url = `https://tmomanga.com/genero/${genre + .toLowerCase() + .replace(" ", "-")}`; + else if (utils.isUsableValue(name)) + url = `https://tmomanga.com/biblioteca?search=${encodeURI(name) + .split("%20") + .join("+")}`; + return null; } /** - * - * @param {string} name - * @param {string} genre - * @param {number} page + * + * @param {string} name + * @param {string} genre + * @param {number} page */ export async function filter(name, genre, page) { - try { - let search_url = getSearchURL(name, genre); - if (search_url != null) { - let mangas = []; - if (utils.isUsableValue(page)) - search_url += `?page=${page}`; - const $ = cheerio.load((await axios.get(search_url)).data); - const elements = $($('div.main-col-inner').get(0)).find('div.row').children(); - for (let i = 0; i < elements.length; i++) - mangas.push($(elements[i]).find('h3 a').attr('href')); - return mangas; - } - } catch (error) { - console.log(error); + try { + let search_url = getSearchURL(name, genre); + if (search_url != null) { + let mangas = []; + if (utils.isUsableValue(page)) search_url += `?page=${page}`; + const $ = cheerio.load((await axios.get(search_url)).data); + const elements = $($("div.main-col-inner").get(0)) + .find("div.row") + .children(); + for (let i = 0; i < elements.length; i++) + mangas.push($(elements[i]).find("h3 a").attr("href")); + return mangas; } + } catch (error) { + console.log(error); + } } diff --git a/src/scraper/sites/news/kudasai/kudasai.js b/src/scraper/sites/news/kudasai/kudasai.js index 79d0ecda..f18cc1b3 100644 --- a/src/scraper/sites/news/kudasai/kudasai.js +++ b/src/scraper/sites/news/kudasai/kudasai.js @@ -2,71 +2,71 @@ import { load } from "cheerio"; import axios from "axios"; import { - Post, - NewsShema, - NewsInfo, + Post, + NewsShema, + NewsInfo, } from "../../../../utils/shemaNewsProviders.js"; //url const kudasaiUrl = { - main: "https://somoskudasai.com/", - posts: "https://somoskudasai.com/noticias", + main: "https://somoskudasai.com/", + posts: "https://somoskudasai.com/noticias", }; //get post async function PostsNews() { - try { - const { data } = await axios.get(kudasaiUrl.posts); - const $ = load(data); - const ShemaDataArray = new NewsShema(); + try { + const { data } = await axios.get(kudasaiUrl.posts); + const $ = load(data); + const ShemaDataArray = new NewsShema(); - //get data post - $("main section div.nwslst article").each((i, e) => { - const cards = new Post(); - cards.title = $(e).find("h2").text().trim(); - cards.topics.push($(e).find("header > span").text().trim().split(" / ")); - cards.image = $("img.attachment-post-thumbnail").attr("src"); - cards.date = $(e).find("header div.ar-mt > span.db").text().trim(); - cards.url = $(e) - .find("a") - .attr("href") - .replace("https://somoskudasai.com/noticias/", "/news/kudasai/"); - ShemaDataArray.data.push(cards); - }); + //get data post + $("main section div.nwslst article").each((i, e) => { + const cards = new Post(); + cards.title = $(e).find("h2").text().trim(); + cards.topics.push($(e).find("header > span").text().trim().split(" / ")); + cards.image = $("img.attachment-post-thumbnail").attr("src"); + cards.date = $(e).find("header div.ar-mt > span.db").text().trim(); + cards.url = $(e) + .find("a") + .attr("href") + .replace("https://somoskudasai.com/noticias/", "/news/kudasai/"); + ShemaDataArray.data.push(cards); + }); - return ShemaDataArray; - } catch (error) { - return error; - } + return ShemaDataArray; + } catch (error) { + return error; + } } //get new info async function New(param) { - try { - const { data } = await axios.get(`${kudasaiUrl.posts}/${param}`); - const $ = load(data); - const ShemaDataArray = new NewsShema(); + try { + const { data } = await axios.get(`${kudasaiUrl.posts}/${param}`); + const $ = load(data); + const ShemaDataArray = new NewsShema(); - //get general info - $("section.single article").each((i, e) => { - const news = new NewsInfo(); - news.title = $(e).find("h1").text().trim(); - news.topics.push($(e).find("span.typ").text().trim().split(" / ")); - news.banner = $(e).find("img").attr("src"); - news.uploadedBy = $(e).find('div.ar-mt span.fwb').text().trim(); - news.uploadedAt = $(e).find('div.ar-mt span.op5').text().trim(); + //get general info + $("section.single article").each((i, e) => { + const news = new NewsInfo(); + news.title = $(e).find("h1").text().trim(); + news.topics.push($(e).find("span.typ").text().trim().split(" / ")); + news.banner = $(e).find("img").attr("src"); + news.uploadedBy = $(e).find("div.ar-mt span.fwb").text().trim(); + news.uploadedAt = $(e).find("div.ar-mt span.op5").text().trim(); - //get info - $("main section div.entry").each((i, e) => { - news.preview.full = $(e).find("p").text().trim(); - news.preview.images.push($(e).find('img').attr('src')); - }); - ShemaDataArray.data.push(news); - }); + //get info + $("main section div.entry").each((i, e) => { + news.preview.full = $(e).find("p").text().trim(); + news.preview.images.push($(e).find("img").attr("src")); + }); + ShemaDataArray.data.push(news); + }); - return ShemaDataArray; - } catch (error) { - return error - } + return ShemaDataArray; + } catch (error) { + return error; + } } /* New( @@ -74,4 +74,4 @@ async function New(param) { ).then((f) => { console.log(f); }); */ -export default {PostsNews, New}; +export default { PostsNews, New }; diff --git a/src/test/Animeflv.spec.ts b/src/test/Animeflv.spec.ts index d75b892a..ef21cf18 100644 --- a/src/test/Animeflv.spec.ts +++ b/src/test/Animeflv.spec.ts @@ -1,26 +1,36 @@ -import {AnimeFlv} from '../scraper/sites/anime/animeflv/AnimeFlv'; -import { StatusAnimeflv, Genres } from '../scraper/sites/anime/animeflv/animeflv_helper'; -describe('AnimeFlv', () => { - let animeFlv: AnimeFlv; - - beforeEach(() => { - animeFlv = new AnimeFlv(); - }); - - it('should get anime info successfully', async () => { - const animeInfo = await animeFlv.GetAnimeInfo('wonder-egg-priority'); - expect(animeInfo.name).toBe('Wonder Egg Priority'); - expect(animeInfo.alt_name).toContain('ワンダーエッグ・プライオリティ'); - expect(animeInfo.image.url).toContain('.jpg'); - expect(animeInfo.status).toBe('En emision'); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); - expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - expect(animeInfo.episodes.length).toBeGreaterThan(0); - }); - - it('should filter anime successfully', async () => { - const result = await animeFlv.Filter(Genres.Action, 'all', 'all', StatusAnimeflv.OnGoing, 1, 1); - expect(result.results.length).toBeGreaterThan(0); - }); - }); \ No newline at end of file +import { AnimeFlv } from "../scraper/sites/anime/animeflv/AnimeFlv"; +import { + StatusAnimeflv, + Genres, +} from "../scraper/sites/anime/animeflv/animeflv_helper"; +describe("AnimeFlv", () => { + let animeFlv: AnimeFlv; + + beforeEach(() => { + animeFlv = new AnimeFlv(); + }); + + it("should get anime info successfully", async () => { + const animeInfo = await animeFlv.GetAnimeInfo("wonder-egg-priority"); + expect(animeInfo.name).toBe("Wonder Egg Priority"); + expect(animeInfo.alt_name).toContain("ワンダーエッグ・プライオリティ"); + expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.status).toBe("En emision"); + expect(animeInfo.synopsis.length).toBeGreaterThan(0); + expect(animeInfo.chronology?.length).toBeGreaterThan(0); + expect(animeInfo.genres.length).toBeGreaterThan(0); + expect(animeInfo.episodes.length).toBeGreaterThan(0); + }); + + it("should filter anime successfully", async () => { + const result = await animeFlv.Filter( + Genres.Action, + "all", + "all", + StatusAnimeflv.OnGoing, + 1, + 1, + ); + expect(result.results.length).toBeGreaterThan(0); + }); +}); diff --git a/src/test/Animelatinohd.spec.ts b/src/test/Animelatinohd.spec.ts index fda6ae6b..2fb063c9 100644 --- a/src/test/Animelatinohd.spec.ts +++ b/src/test/Animelatinohd.spec.ts @@ -22,4 +22,4 @@ describe("AnimeLatinohd", () => { const result = await animelatinohd.GetAnimeByFilter(); expect(result.results.length).toBeGreaterThan(0); }, 10000); -}); \ No newline at end of file +}); diff --git a/src/test/Comick.spec.ts b/src/test/Comick.spec.ts index 559ef27e..98624d1d 100644 --- a/src/test/Comick.spec.ts +++ b/src/test/Comick.spec.ts @@ -9,15 +9,14 @@ describe("Comick", () => { it("should get anime info successfully", async () => { const mangaInfo = await comick.GetMangaInfo("00-solo-leveling", "en"); - + expect(mangaInfo.title).toBe("Solo Leveling"); expect(mangaInfo.altTitles).toContain("我独自升级"); expect(mangaInfo.status).toBe("completed"); - }); it("should filter anime successfully", async () => { const result = await comick.GetMangaByFilter(); expect(result.results.length).toBeGreaterThan(0); }, 10000); -}); \ No newline at end of file +}); diff --git a/src/test/Inmanga.spec.ts b/src/test/Inmanga.spec.ts index c3b80172..c6ae2eda 100644 --- a/src/test/Inmanga.spec.ts +++ b/src/test/Inmanga.spec.ts @@ -9,16 +9,15 @@ describe("Inmanga", () => { it("should get anime info successfully", async () => { const mangaInfo = await inmanga.GetMangaInfo("Kimetsu-no-Yaiba"); - + expect(mangaInfo.title).toBe("Kimetsu no Yaiba"); expect(mangaInfo.altTitles).toContain("Blade of Demon Destruction"); expect(mangaInfo.status).toBe("ongoing"); - }); it("should filter anime successfully", async () => { const result = await inmanga.GetMangaByFilter(); expect(result.results.length).toBeGreaterThan(0); }, 10000); -}); \ No newline at end of file +}); diff --git a/src/test/MangaReader.spec.ts b/src/test/MangaReader.spec.ts index 93ce63b9..ca430d44 100644 --- a/src/test/MangaReader.spec.ts +++ b/src/test/MangaReader.spec.ts @@ -6,7 +6,7 @@ import { MangaReaderFilterScore, MangaReaderFilterStatus, MangaReaderFilterLanguage, - MangaReaderFilterRatingType + MangaReaderFilterRatingType, } from "../scraper/sites/manga/MangaReader/MangaReaderTypes"; describe("MangaReader", () => { @@ -27,60 +27,60 @@ describe("MangaReader", () => { hasVolumes: boolean; hasChapters: boolean; }> = [ - { - id: 65961, - mangaName: "Zashisu", - altName: ["ザシス"], - mangaGenres: ["Horror", "Mystery", "Psychological", "School", "Seinen"], - isNsfw: false, - status: "ongoing", - hasVolumes: false, - hasChapters: true - }, - { - id: 65941, - mangaName: "Mitsuba no Monogatari", - altName: ["みつばものがたり 呪いの少女と死の輪舞《ロンド》"], - mangaGenres: ["Fantasy"], - isNsfw: false, - status: "ongoing", - hasVolumes: false, - hasChapters: true - }, - { - id: 65795, - mangaName: - "Akuyaku Reijou ni Tensei suru no Mahou ni Muchuu de Itara Ouji ni Dekiaisaremashita", - altName: ["悪役令嬢に転生するも魔法に夢中でいたら王子に溺愛されました"], - mangaGenres: ["Fantasy", "Romance", "School", "Shoujo"], - isNsfw: false, - status: "ongoing", - hasVolumes: false, - hasChapters: true - }, - { - id: 65879, - mangaName: "My Star Is the Lewdest", - altName: ["俺の女優が一番淫ら"], - mangaGenres: ["Comedy", "Ecchi"], - isNsfw: true, - status: "ongoing", - hasVolumes: false, - hasChapters: true - }, - { - id: 65789, - mangaName: "Hoop Days", - altName: ["ディアボーイズ"], - mangaGenres: ["Drama", "Slice of Life", "Sports"], - isNsfw: false, - status: "completed", - hasVolumes: false, - hasChapters: true - } - ]; + { + id: 65961, + mangaName: "Zashisu", + altName: ["ザシス"], + mangaGenres: ["Horror", "Mystery", "Psychological", "School", "Seinen"], + isNsfw: false, + status: "ongoing", + hasVolumes: false, + hasChapters: true, + }, + { + id: 65941, + mangaName: "Mitsuba no Monogatari", + altName: ["みつばものがたり 呪いの少女と死の輪舞《ロンド》"], + mangaGenres: ["Fantasy"], + isNsfw: false, + status: "ongoing", + hasVolumes: false, + hasChapters: true, + }, + { + id: 65795, + mangaName: + "Akuyaku Reijou ni Tensei suru no Mahou ni Muchuu de Itara Ouji ni Dekiaisaremashita", + altName: ["悪役令嬢に転生するも魔法に夢中でいたら王子に溺愛されました"], + mangaGenres: ["Fantasy", "Romance", "School", "Shoujo"], + isNsfw: false, + status: "ongoing", + hasVolumes: false, + hasChapters: true, + }, + { + id: 65879, + mangaName: "My Star Is the Lewdest", + altName: ["俺の女優が一番淫ら"], + mangaGenres: ["Comedy", "Ecchi"], + isNsfw: true, + status: "ongoing", + hasVolumes: false, + hasChapters: true, + }, + { + id: 65789, + mangaName: "Hoop Days", + altName: ["ディアボーイズ"], + mangaGenres: ["Drama", "Slice of Life", "Sports"], + isNsfw: false, + status: "completed", + hasVolumes: false, + hasChapters: true, + }, + ]; - testsList.forEach(async fields => { + testsList.forEach(async (fields) => { const { id, mangaName, @@ -89,7 +89,7 @@ describe("MangaReader", () => { isNsfw, status, hasVolumes, - hasChapters + hasChapters, } = fields; const mangaInfo = await mangareader.GetMangaInfo(id); @@ -116,7 +116,7 @@ describe("MangaReader", () => { status?: MangaReaderFilterStatus; ratingType?: MangaReaderFilterRatingType; score?: MangaReaderFilterScore; - language?: typeof MangaReaderFilterLanguage[number]; + language?: (typeof MangaReaderFilterLanguage)[number]; startYear?: number; startMonth?: number; startDay?: number; @@ -126,63 +126,63 @@ describe("MangaReader", () => { sort?: MangaReaderFilterSort; numPage?: number; }> = [ - { - hasResults: true, - type: MangaReaderFilterType.Manhwa, - status: MangaReaderFilterStatus.Finished, - ratingType: MangaReaderFilterRatingType.MildNudity, - numPage: 1 - }, - { - hasResults: true, - type: MangaReaderFilterType.Doujinshi, - status: MangaReaderFilterStatus.All, - ratingType: MangaReaderFilterRatingType.Teens, - score: MangaReaderFilterScore.Horrible, - language: "ja" - }, - { - hasResults: true, - type: MangaReaderFilterType.Manga, - status: MangaReaderFilterStatus.Finished, - ratingType: MangaReaderFilterRatingType.Teens, - score: MangaReaderFilterScore.All, - language: "ja", - startYear: 2021, - startMonth: 3, - startDay: 5, - endYear: 2023, - endMonth: 3, - endDay: 6 - }, - { - hasResults: true, - type: MangaReaderFilterType.OneShot, - status: MangaReaderFilterStatus.All, - ratingType: MangaReaderFilterRatingType.Teens, - numPage: 2 - }, - { - hasResults: false, - type: MangaReaderFilterType.LightNovel, - status: MangaReaderFilterStatus.Finished, - ratingType: MangaReaderFilterRatingType.Children, - score: MangaReaderFilterScore.All, - language: "en", - numPage: 3 - }, - { - hasResults: false, - type: MangaReaderFilterType.OneShot, - status: MangaReaderFilterStatus.All, - ratingType: MangaReaderFilterRatingType.Teens, - score: MangaReaderFilterScore.VeryGood, - language: "en", - numPage: 1 - } - ]; + { + hasResults: true, + type: MangaReaderFilterType.Manhwa, + status: MangaReaderFilterStatus.Finished, + ratingType: MangaReaderFilterRatingType.MildNudity, + numPage: 1, + }, + { + hasResults: true, + type: MangaReaderFilterType.Doujinshi, + status: MangaReaderFilterStatus.All, + ratingType: MangaReaderFilterRatingType.Teens, + score: MangaReaderFilterScore.Horrible, + language: "ja", + }, + { + hasResults: true, + type: MangaReaderFilterType.Manga, + status: MangaReaderFilterStatus.Finished, + ratingType: MangaReaderFilterRatingType.Teens, + score: MangaReaderFilterScore.All, + language: "ja", + startYear: 2021, + startMonth: 3, + startDay: 5, + endYear: 2023, + endMonth: 3, + endDay: 6, + }, + { + hasResults: true, + type: MangaReaderFilterType.OneShot, + status: MangaReaderFilterStatus.All, + ratingType: MangaReaderFilterRatingType.Teens, + numPage: 2, + }, + { + hasResults: false, + type: MangaReaderFilterType.LightNovel, + status: MangaReaderFilterStatus.Finished, + ratingType: MangaReaderFilterRatingType.Children, + score: MangaReaderFilterScore.All, + language: "en", + numPage: 3, + }, + { + hasResults: false, + type: MangaReaderFilterType.OneShot, + status: MangaReaderFilterStatus.All, + ratingType: MangaReaderFilterRatingType.Teens, + score: MangaReaderFilterScore.VeryGood, + language: "en", + numPage: 1, + }, + ]; - testsList.forEach(async fields => { + testsList.forEach(async (fields) => { const { hasResults, type, @@ -197,7 +197,7 @@ describe("MangaReader", () => { endMonth, endDay, sort, - numPage + numPage, } = fields; const filter = await mangareader.Filter({ @@ -213,7 +213,7 @@ describe("MangaReader", () => { endMonth: endMonth, endDay: endDay, sort: sort, - numPage: numPage + numPage: numPage, }); if (hasResults === true) @@ -227,32 +227,32 @@ describe("MangaReader", () => { chapterTitle: string; id: number; chapterNumber: number; - language: typeof MangaReaderFilterLanguage[number]; + language: (typeof MangaReaderFilterLanguage)[number]; type: MangaReaderChapterType; }> = [ - { - chapterTitle: "Chapter 3: 第 3 話", - id: 65953, - chapterNumber: 3, - language: "ja", - type: "chapter" - }, - { - chapterTitle: "VOL 2", - id: 65781, - chapterNumber: 2, - language: "en", - type: "volume" - } - ]; + { + chapterTitle: "Chapter 3: 第 3 話", + id: 65953, + chapterNumber: 3, + language: "ja", + type: "chapter", + }, + { + chapterTitle: "VOL 2", + id: 65781, + chapterNumber: 2, + language: "en", + type: "volume", + }, + ]; - testsList.forEach(async fields => { + testsList.forEach(async (fields) => { const { chapterTitle, id, chapterNumber, language, type } = fields; const mangaChapters = await mangareader.GetMangaChapters( id, chapterNumber, language, - type + type, ); expect(mangaChapters?.images.length).toBeGreaterThanOrEqual(1); diff --git a/src/test/Manganelo.spec.ts b/src/test/Manganelo.spec.ts index 6353a9f6..4a5125a2 100644 --- a/src/test/Manganelo.spec.ts +++ b/src/test/Manganelo.spec.ts @@ -1,5 +1,8 @@ -import { Manganelo } from '../scraper/sites/manga/manganelo/Manganelo'; -import { IManganatoFilterParams, manganatoGenreList } from '../scraper/sites/manga/manganelo/ManganatoTypes'; +import { Manganelo } from "../scraper/sites/manga/manganelo/Manganelo"; +import { + IManganatoFilterParams, + manganatoGenreList, +} from "../scraper/sites/manga/manganelo/ManganatoTypes"; type ManganeloTestTemplate = { id: string; @@ -10,49 +13,49 @@ type ManganeloTestTemplate = { type ManganeloGenresOptions = keyof typeof manganatoGenreList; -interface ManganeloFilterTestTemplate extends Omit { +interface ManganeloFilterTestTemplate + extends Omit { genres: ManganeloGenresOptions[]; -}; +} type ManganeloChapterTestTemplate = { id: string; num: number; }; -describe('Manganelo', () => { +describe("Manganelo", () => { let manganelo: Manganelo; beforeEach(() => { manganelo = new Manganelo(); }); - it('should get manga info successfully', async () => { - + it("should get manga info successfully", async () => { const testsSuites: ManganeloTestTemplate[] = [ { - id: 'md990312', + id: "md990312", nsfw: false, - status: 'ongoing', - title: 'Your Eternal Lies' + status: "ongoing", + title: "Your Eternal Lies", }, { - id: 'he984887', + id: "he984887", nsfw: false, - status: 'ongoing', - title: 'The Peerless Sword God' + status: "ongoing", + title: "The Peerless Sword God", }, { - id: 'go983949', + id: "go983949", nsfw: false, - status: 'ongoing', - title: 'Bite Into Me' + status: "ongoing", + title: "Bite Into Me", }, { - id: 'oj992266', + id: "oj992266", nsfw: true, - status: 'ongoing', - title: 'Dekiai Osananajimi Ha Watashi No Otto De Stalker!?' - } + status: "ongoing", + title: "Dekiai Osananajimi Ha Watashi No Otto De Stalker!?", + }, ]; testsSuites.forEach(async (options) => { @@ -63,7 +66,7 @@ describe('Manganelo', () => { expect(mangaInfo.altTitles.length).toBeGreaterThanOrEqual(1); if (mangaInfo.thumbnail && mangaInfo.thumbnail.url) - expect(mangaInfo.thumbnail.url).toContain('.jpg'); + expect(mangaInfo.thumbnail.url).toContain(".jpg"); expect(mangaInfo.status).toStrictEqual(options.status); expect(mangaInfo.isNSFW).toStrictEqual(options.nsfw); @@ -76,52 +79,52 @@ describe('Manganelo', () => { }); }); - it('should filter manga successfully', async () => { + it("should filter manga successfully", async () => { const filterTestsSuites: ManganeloFilterTestTemplate[] = [ { - genres: ['action'], - orby: 'az', + genres: ["action"], + orby: "az", page: 3, - sts: 'completed' + sts: "completed", }, { - genres: ['drama', 'romance'], - orby: 'newest', + genres: ["drama", "romance"], + orby: "newest", page: 1, - sts: 'ongoing' - } + sts: "ongoing", + }, ]; filterTestsSuites.forEach(async (options) => { const result = await manganelo.Filter({ - genres: options.genres.join(' '), + genres: options.genres.join(" "), orby: options.orby, page: options.page, - sts: options.sts + sts: options.sts, }); expect(result.results.length).toBeGreaterThanOrEqual(1); }); }); - it('should return manga chapters successfully', async () => { + it("should return manga chapters successfully", async () => { const chapterTestsSuites: ManganeloChapterTestTemplate[] = [ { - id: 'he984887', - num: 221 + id: "he984887", + num: 221, }, { - id: 'oj992266', - num: 1 + id: "oj992266", + num: 1, }, { - id: 'md990312', - num: 79 + id: "md990312", + num: 79, }, { - id: 'go983949', - num: 2 - } + id: "go983949", + num: 2, + }, ]; chapterTestsSuites.forEach(async (options) => { diff --git a/src/test/TioAnime.spec.ts b/src/test/TioAnime.spec.ts index 739520ca..0b96b993 100644 --- a/src/test/TioAnime.spec.ts +++ b/src/test/TioAnime.spec.ts @@ -1,16 +1,18 @@ -import { TioAnime } from '../scraper/sites/anime/tioanime/TioAnime' +import { TioAnime } from "../scraper/sites/anime/tioanime/TioAnime"; describe("TioAnime", () => { let tioanime: TioAnime; beforeEach(() => { tioanime = new TioAnime(); - }) - it('should get anime info successfully', async () => { - const animeInfo = await tioanime.getAnime('https://tioanime.com/anime/date-a-live'); + }); + it("should get anime info successfully", async () => { + const animeInfo = await tioanime.getAnime( + "https://tioanime.com/anime/date-a-live", + ); - expect(animeInfo.name).toBe('Date A Live'); - expect(animeInfo.image.url).toContain('.jpg'); + expect(animeInfo.name).toBe("Date A Live"); + expect(animeInfo.image.url).toContain(".jpg"); expect(animeInfo.synopsis.length).toBeGreaterThan(0); expect(animeInfo.chronology?.length).toBeGreaterThan(0); expect(animeInfo.genres.length).toBeGreaterThan(0); @@ -33,4 +35,4 @@ describe("TioAnime", () => { const result = await tioanime.filter("", ["1"], ["accion"], { begin: 1950, end: 2023 }, 2, "recent"); expect(result.results.length).toBeGreaterThan(0); });*/ -}) +}); diff --git a/src/test/Zoro.spec.ts b/src/test/Zoro.spec.ts index 1e94db83..e4f9ab4c 100644 --- a/src/test/Zoro.spec.ts +++ b/src/test/Zoro.spec.ts @@ -1,22 +1,22 @@ -import {Zoro} from '../scraper/sites/anime/zoro/Zoro' +import { Zoro } from "../scraper/sites/anime/zoro/Zoro"; describe("Zoro", () => { - let zoro: Zoro; + let zoro: Zoro; - beforeEach(()=> { - zoro = new Zoro(); - }) - it('should get anime info successfully', async () => { - const animeInfo = await zoro.GetAnimeInfo('tokyo-ghoul-790'); - expect(animeInfo.name).toBe('Tokyo Ghoul'); - expect(animeInfo.alt_name).toContain('東京喰種-トーキョーグール-'); - expect(animeInfo.image.url).toContain('.jpg'); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); - expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - }); - it('should filter anime successfully', async () => { - const result = await zoro.Filter("2"); - expect(result.results.length).toBeGreaterThan(0); - }); -}) + beforeEach(() => { + zoro = new Zoro(); + }); + it("should get anime info successfully", async () => { + const animeInfo = await zoro.GetAnimeInfo("tokyo-ghoul-790"); + expect(animeInfo.name).toBe("Tokyo Ghoul"); + expect(animeInfo.alt_name).toContain("東京喰種-トーキョーグール-"); + expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.synopsis.length).toBeGreaterThan(0); + expect(animeInfo.chronology?.length).toBeGreaterThan(0); + expect(animeInfo.genres.length).toBeGreaterThan(0); + }); + it("should filter anime successfully", async () => { + const result = await zoro.Filter("2"); + expect(result.results.length).toBeGreaterThan(0); + }); +}); diff --git a/src/types/anime.ts b/src/types/anime.ts index 1fb02fde..5d6b4c20 100644 --- a/src/types/anime.ts +++ b/src/types/anime.ts @@ -1,154 +1,154 @@ -//anime data return standard - -import { ICalendar, IDatePeriod } from './date'; -import { IEpisode } from './episode'; -import { IImage } from './image'; - -//spanish providers - TypeScript version - -/** Specifies the type of anime to which its content refers. */ -export type AnimeType = "Anime" | "Movie" | "OVA" | "ONA" | "Null"; -/** Specify the climatic season in which the anime was published. */ -export type ClimaticStation = "Summer" | "Autumn" | "Winter" | "Spring"; - -/** - * Spectify the rating and stats in the anime - * @author Zukaritasu - */ -export interface IAnimeStats { - score?: string | number; - views?: string | number; - rating?: string | number; // stars -} - -/** - * Spectify chronology to that anime, in some pages puts what anime - * should you see before to that anime - * - * @author Mawfyy - */ -export interface IChronology { - name: string; - url: `/anime/${string}/name/${string}`| string; - image?: string; -} - -/** - * Spectify the anime structure that you scrapped - * @author Zukaritasu - */ -export interface IAnime { - /** Name of the anime */ - name: string; - /** Alternative names describing the name of the anime in another language */ - alt_name?: string | string[]; - /** Anime identifier that can be used when the anime name is not used in the URL. */ - id?: number; - /** The URL or location of the anime in the API */ - url: `/anime/${string}/name/${string}` | string; - /** The anime synopsis */ - synopsis?: string; - /** - * An IImage interface object representing the anime - * image and its banner. */ - image: IImage; - /** - * The date from when the anime started until it ended. The end date may be - * auxiliary in case the anime has not ended. */ - date?: IDatePeriod | ICalendar; - /** The type of anime that indicates whether it is a movie, a special, TV, etc.. */ - type?: AnimeType; - /** Genres that apply to anime */ - genres?: string[]; - /** Climatic station of which the anime was released */ - station?: ClimaticStation | string; - /** - * Most anime websites have an anime statistics section including ratings and - * number of views, etc... */ - stats?: IAnimeStats; - /** Chronology of the anime. It is an array that contains the anime related to it. */ - chronology?: IChronology[]; - /** - * A list of the episodes of this anime. This property must be null or not used - * if an IAnime object is used in IChronology. */ - episodes?: IEpisode[]; - /** - * The status of the anime indicating whether it is on air, finished - * or still on hold. */ - status?: string | boolean; - /** Indicates whether the anime is adult content. */ - nsfw?: boolean; -} - -/**---------------- Interfaces implementation ---------------- **/ - -/** - * Spectify the rating and stats in the anime - * @author Zukaritasu - */ -export class AnimeStats implements IAnimeStats { - /** Anime score */ - score?: string | number; - /** The number of views of the anime */ - views?: string | number; - /** */ - rating?: string | number; -} - -/** - * Spectify chronology to that anime, in some pages puts what anime - * should you see before to that anime - * - * @author Mawfyy - */ -export class Chronology implements IChronology { - /** @inheritdoc */ - name: string; - /** @inheritdoc */ - url: `/anime/${string}/name/${string}` | string; - /** @inheritdoc */ - image?: string; - - constructor(name?: string, url?: string, image?: string) { - this.name = name; - this.url = url; - this.image = image; - } -} - -/** - * Spectify the anime structure that you scrapped - * @author Zukaritasu - */ -export class Anime implements IAnime { - /** @inheritdoc */ - name: string; - /** @inheritdoc */ - alt_name?: string | string[]; - /** @inheritdoc */ - id?: number; - /** @inheritdoc */ - url: `/anime/${string}/name/${string}` | string; - /** @inheritdoc */ - synopsis: string; - /** @inheritdoc */ - image: IImage; - /** @inheritdoc */ - date?: IDatePeriod | ICalendar; - /** @inheritdoc */ - type?: AnimeType; - /** @inheritdoc */ - genres: string[] = []; - /** @inheritdoc */ - stats?: IAnimeStats; - /** @inheritdoc */ - station?: ClimaticStation | string; - /** @inheritdoc */ - chronology?: IChronology[]; - /** @inheritdoc */ - episodes: IEpisode[] = []; - /** @inheritdoc */ - status?: string | boolean; - /** @inheritdoc */ - nsfw?: boolean; -} +//anime data return standard + +import { ICalendar, IDatePeriod } from "./date"; +import { IEpisode } from "./episode"; +import { IImage } from "./image"; + +//spanish providers - TypeScript version + +/** Specifies the type of anime to which its content refers. */ +export type AnimeType = "Anime" | "Movie" | "OVA" | "ONA" | "Null"; +/** Specify the climatic season in which the anime was published. */ +export type ClimaticStation = "Summer" | "Autumn" | "Winter" | "Spring"; + +/** + * Spectify the rating and stats in the anime + * @author Zukaritasu + */ +export interface IAnimeStats { + score?: string | number; + views?: string | number; + rating?: string | number; // stars +} + +/** + * Spectify chronology to that anime, in some pages puts what anime + * should you see before to that anime + * + * @author Mawfyy + */ +export interface IChronology { + name: string; + url: `/anime/${string}/name/${string}` | string; + image?: string; +} + +/** + * Spectify the anime structure that you scrapped + * @author Zukaritasu + */ +export interface IAnime { + /** Name of the anime */ + name: string; + /** Alternative names describing the name of the anime in another language */ + alt_name?: string | string[]; + /** Anime identifier that can be used when the anime name is not used in the URL. */ + id?: number; + /** The URL or location of the anime in the API */ + url: `/anime/${string}/name/${string}` | string; + /** The anime synopsis */ + synopsis?: string; + /** + * An IImage interface object representing the anime + * image and its banner. */ + image: IImage; + /** + * The date from when the anime started until it ended. The end date may be + * auxiliary in case the anime has not ended. */ + date?: IDatePeriod | ICalendar; + /** The type of anime that indicates whether it is a movie, a special, TV, etc.. */ + type?: AnimeType; + /** Genres that apply to anime */ + genres?: string[]; + /** Climatic station of which the anime was released */ + station?: ClimaticStation | string; + /** + * Most anime websites have an anime statistics section including ratings and + * number of views, etc... */ + stats?: IAnimeStats; + /** Chronology of the anime. It is an array that contains the anime related to it. */ + chronology?: IChronology[]; + /** + * A list of the episodes of this anime. This property must be null or not used + * if an IAnime object is used in IChronology. */ + episodes?: IEpisode[]; + /** + * The status of the anime indicating whether it is on air, finished + * or still on hold. */ + status?: string | boolean; + /** Indicates whether the anime is adult content. */ + nsfw?: boolean; +} + +/**---------------- Interfaces implementation ---------------- **/ + +/** + * Spectify the rating and stats in the anime + * @author Zukaritasu + */ +export class AnimeStats implements IAnimeStats { + /** Anime score */ + score?: string | number; + /** The number of views of the anime */ + views?: string | number; + /** */ + rating?: string | number; +} + +/** + * Spectify chronology to that anime, in some pages puts what anime + * should you see before to that anime + * + * @author Mawfyy + */ +export class Chronology implements IChronology { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + url: `/anime/${string}/name/${string}` | string; + /** @inheritdoc */ + image?: string; + + constructor(name?: string, url?: string, image?: string) { + this.name = name; + this.url = url; + this.image = image; + } +} + +/** + * Spectify the anime structure that you scrapped + * @author Zukaritasu + */ +export class Anime implements IAnime { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + alt_name?: string | string[]; + /** @inheritdoc */ + id?: number; + /** @inheritdoc */ + url: `/anime/${string}/name/${string}` | string; + /** @inheritdoc */ + synopsis: string; + /** @inheritdoc */ + image: IImage; + /** @inheritdoc */ + date?: IDatePeriod | ICalendar; + /** @inheritdoc */ + type?: AnimeType; + /** @inheritdoc */ + genres: string[] = []; + /** @inheritdoc */ + stats?: IAnimeStats; + /** @inheritdoc */ + station?: ClimaticStation | string; + /** @inheritdoc */ + chronology?: IChronology[]; + /** @inheritdoc */ + episodes: IEpisode[] = []; + /** @inheritdoc */ + status?: string | boolean; + /** @inheritdoc */ + nsfw?: boolean; +} diff --git a/src/types/date.ts b/src/types/date.ts index 2f635189..0829f895 100644 --- a/src/types/date.ts +++ b/src/types/date.ts @@ -1,90 +1,94 @@ -//Spanish Providers - TypeScript version - -/** - * In most anime websites in the anime information part may be available - * the year in which the content was published but without specifying the - * month and day so the year property is not optional, although from the - * HTML in some cases you can extract the exact month and day of publication, - * in that case the month and day properties are optional. - * - * @author Zukaritasu - */ -export interface ICalendar { - /** - * The year of publication. This property is not optional - * because in many anime pages they only say the year of publication but - * not the month and day. */ - year: number | string; - month?: number | string; - day?: number | string; -} - -/** - * If the anime was completed it has a start and end date of - * publication but if it is still on air only the begin property will contain - * the information of when it was published, the end property is optional. - * - * @author Zukaritasu - */ -export interface IDatePeriod { - /** The exact date on which it was published the anime */ - begin: ICalendar; - /** - * The exact date the anime ended. Ownership is optional because it - * may not be finished yet. */ - end?: ICalendar; -} - -/** - * Specifies the year, month and day in which the anime or other content - * related to the anime was released in the form of a movie, OVA or ONA. - * Implementation of the {@link ICalendar} interface - * - * @author Zukaritasu - * @extends ICalendar - */ -export class Calendar implements ICalendar { - /** @inheritdoc */ - year: number; - /** @inheritdoc */ - month?: number; - /** @inheritdoc */ - day?: number; - - constructor(year: number) { - this.year = year; - } - - /** - * Returns an instance of the ICalendar interface. The - * function takes as parameter the date in a string; the format - * must be compatible with the constructor of the Date class. - * @example - * Calendar.getCalendar("01/05/2022") - * - * @param date - * @returns ICalendar - */ - static getCalendar(date: string): ICalendar { - const obj = new Date(date); - return { year: obj.getFullYear(), day: obj.getDay(), month: obj.getMonth() }; - } -} - -/** - * Implementation of the {@link IDatePeriod} interface - * - * @author Zukaritasu - * @extends IDatePeriod - */ -export class DatePeriod implements IDatePeriod { - /** @inheritdoc */ - begin: ICalendar; - /** @inheritdoc */ - end?: ICalendar; - - constructor(begin: ICalendar, end?: ICalendar) { - this.begin = begin; - this.end = end; - } -} +//Spanish Providers - TypeScript version + +/** + * In most anime websites in the anime information part may be available + * the year in which the content was published but without specifying the + * month and day so the year property is not optional, although from the + * HTML in some cases you can extract the exact month and day of publication, + * in that case the month and day properties are optional. + * + * @author Zukaritasu + */ +export interface ICalendar { + /** + * The year of publication. This property is not optional + * because in many anime pages they only say the year of publication but + * not the month and day. */ + year: number | string; + month?: number | string; + day?: number | string; +} + +/** + * If the anime was completed it has a start and end date of + * publication but if it is still on air only the begin property will contain + * the information of when it was published, the end property is optional. + * + * @author Zukaritasu + */ +export interface IDatePeriod { + /** The exact date on which it was published the anime */ + begin: ICalendar; + /** + * The exact date the anime ended. Ownership is optional because it + * may not be finished yet. */ + end?: ICalendar; +} + +/** + * Specifies the year, month and day in which the anime or other content + * related to the anime was released in the form of a movie, OVA or ONA. + * Implementation of the {@link ICalendar} interface + * + * @author Zukaritasu + * @extends ICalendar + */ +export class Calendar implements ICalendar { + /** @inheritdoc */ + year: number; + /** @inheritdoc */ + month?: number; + /** @inheritdoc */ + day?: number; + + constructor(year: number) { + this.year = year; + } + + /** + * Returns an instance of the ICalendar interface. The + * function takes as parameter the date in a string; the format + * must be compatible with the constructor of the Date class. + * @example + * Calendar.getCalendar("01/05/2022") + * + * @param date + * @returns ICalendar + */ + static getCalendar(date: string): ICalendar { + const obj = new Date(date); + return { + year: obj.getFullYear(), + day: obj.getDay(), + month: obj.getMonth(), + }; + } +} + +/** + * Implementation of the {@link IDatePeriod} interface + * + * @author Zukaritasu + * @extends IDatePeriod + */ +export class DatePeriod implements IDatePeriod { + /** @inheritdoc */ + begin: ICalendar; + /** @inheritdoc */ + end?: ICalendar; + + constructor(begin: ICalendar, end?: ICalendar) { + this.begin = begin; + this.end = end; + } +} diff --git a/src/types/episode.ts b/src/types/episode.ts index 53690fe9..41e61eae 100644 --- a/src/types/episode.ts +++ b/src/types/episode.ts @@ -1,84 +1,84 @@ -//Spanish Providers - TypeScript version - -/** - * This interface only puts the server name where host episode, - * and url to that episode - * - * @author Mawfyy - * @author Zukaritasu - */ -export interface IEpisodeServer { - /** Name of the server where the episode is hosted */ - name: string; - /** - * The URL of the chapter. This URL leads to the video player of the - * server where the episode is hosted. */ - url: string; - /** Direct video file url for download */ - file_url?: string; -} - -/** - * This interface of the episode contains the basic properties necessary - * for the scraper. The *servers* property contains a list of servers - * although this is optional for performance reasons when using the API. - * - * @author Zukaritasu - */ -export interface IEpisode { - /** - * Name of anime episode. May contain the chapter number concatenated - * with the anime name. */ - name: string; - /** The episode URL in the API query */ - url: `/anime/${string}/episode/${string | number}` | string; - /** The episode number. By default the value can be 0 in string or integer. */ - number: number | string; - /** - * List of available servers where the episode is located. Remember that - * this is not the download link of the episode but of the video player. */ - servers?: IEpisodeServer[]; - /** The image of the episode shown as thumbnail */ - image: string; -} - -/** - * This interface only puts the server name where host episode, - * and url to that episode - * - * @author Mawfyy - * @author Zukaritasu - */ -export class EpisodeServer implements IEpisodeServer { - /** @inheritdoc */ - name: string; - /** @inheritdoc */ - url: string; - /** @inheritdoc */ - file_url?: string; - - constructor(name?: string, url?: string) { - this.name = name; - this.url = url; - } -} - -/** - * This interface of the episode contains the basic properties necessary - * for the scraper. The *servers* property contains a list of servers - * although this is optional for performance reasons when using the API. - * - * @author Zukaritasu - */ -export class Episode implements IEpisode { - /** @inheritdoc */ - name: string; - /** @inheritdoc */ - url: `/anime/${string}/episode/${string | number}` | string; - /** @inheritdoc */ - number: number | string; - /** @inheritdoc */ - servers?: IEpisodeServer[] = []; - /** @inheritdoc */ - image: string; -} +//Spanish Providers - TypeScript version + +/** + * This interface only puts the server name where host episode, + * and url to that episode + * + * @author Mawfyy + * @author Zukaritasu + */ +export interface IEpisodeServer { + /** Name of the server where the episode is hosted */ + name: string; + /** + * The URL of the chapter. This URL leads to the video player of the + * server where the episode is hosted. */ + url: string; + /** Direct video file url for download */ + file_url?: string; +} + +/** + * This interface of the episode contains the basic properties necessary + * for the scraper. The *servers* property contains a list of servers + * although this is optional for performance reasons when using the API. + * + * @author Zukaritasu + */ +export interface IEpisode { + /** + * Name of anime episode. May contain the chapter number concatenated + * with the anime name. */ + name: string; + /** The episode URL in the API query */ + url: `/anime/${string}/episode/${string | number}` | string; + /** The episode number. By default the value can be 0 in string or integer. */ + number: number | string; + /** + * List of available servers where the episode is located. Remember that + * this is not the download link of the episode but of the video player. */ + servers?: IEpisodeServer[]; + /** The image of the episode shown as thumbnail */ + image: string; +} + +/** + * This interface only puts the server name where host episode, + * and url to that episode + * + * @author Mawfyy + * @author Zukaritasu + */ +export class EpisodeServer implements IEpisodeServer { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + url: string; + /** @inheritdoc */ + file_url?: string; + + constructor(name?: string, url?: string) { + this.name = name; + this.url = url; + } +} + +/** + * This interface of the episode contains the basic properties necessary + * for the scraper. The *servers* property contains a list of servers + * although this is optional for performance reasons when using the API. + * + * @author Zukaritasu + */ +export class Episode implements IEpisode { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + url: `/anime/${string}/episode/${string | number}` | string; + /** @inheritdoc */ + number: number | string; + /** @inheritdoc */ + servers?: IEpisodeServer[] = []; + /** @inheritdoc */ + image: string; +} diff --git a/src/types/extractors.ts b/src/types/extractors.ts index 56548a4e..efaca982 100644 --- a/src/types/extractors.ts +++ b/src/types/extractors.ts @@ -3,41 +3,38 @@ import axios from "axios"; //import { UnPacked } from "./utils"; /** - * + * * @param url URL to Request data * @example await filemoon("https://filemoon.sx/e/5ehdd8cohg8r") * @description List of domains [filemoon.sx] - * + * * RequestBR Preload url needed by the request with cookies */ - -axios.defaults.withCredentials = true - +axios.defaults.withCredentials = true; export const filemoon = async (_url: string) => { - - //const Request = await axios.get("https://filemoon.sx/e/5ehdd8cohg8r") - let headersList = { - "User-Agent":"Mozilla/5.0 (Linux; Android 10; LM-K920) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36" - } - - let response = await fetch("https://filemoon.sx/e/397bb6qxbwvh", { - method: "GET", - credentials:"include", - headers: headersList - }); - - let data = await response.text(); - - console.log(btoa(data)) - // const $ = cheerio.load(data) - - //const Buffer = btoa($("script").get().at(-1).children[0].data) - //const UnBuffer = UnPacked(Buffer) - - //const RequestBR = await eval(UnBuffer.slice(UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, UnBuffer.indexOf("}],image:", 1))); - //await axios.get(RequestBR,{headers:{"Accept":"*/*","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"}}) - return data - -} + //const Request = await axios.get("https://filemoon.sx/e/5ehdd8cohg8r") + let headersList = { + "User-Agent": + "Mozilla/5.0 (Linux; Android 10; LM-K920) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Mobile Safari/537.36", + }; + + let response = await fetch("https://filemoon.sx/e/397bb6qxbwvh", { + method: "GET", + credentials: "include", + headers: headersList, + }); + + let data = await response.text(); + + console.log(btoa(data)); + // const $ = cheerio.load(data) + + //const Buffer = btoa($("script").get().at(-1).children[0].data) + //const UnBuffer = UnPacked(Buffer) + + //const RequestBR = await eval(UnBuffer.slice(UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, UnBuffer.indexOf("}],image:", 1))); + //await axios.get(RequestBR,{headers:{"Accept":"*/*","User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"}}) + return data; +}; diff --git a/src/types/filter.ts b/src/types/filter.ts index 9c118a00..e646abf9 100644 --- a/src/types/filter.ts +++ b/src/types/filter.ts @@ -2,44 +2,44 @@ * @author Yako */ export enum Genres { - Action = "Acción", - MartialArts = "Artes Marciales", - Adventure = "Aventuras", - Racing = "Carreras", - ScienceFiction = "Ciencia Ficción", - Comedy = "Comedia", - Dementia = "Demencia", - Demons = "Demonios", - Sports = "Deportes", - Drama = "Drama", - Ecchi = "Ecchi", - School = "Escolares", - Space = "Espacial", - Fantasy = "Fantasía", - Harem = "Harem", - Historical = "Histórico", - Kids = "Infantil", - Josei = "Josei", - Games = "Juegos", - Magic = "Magia", - Mecha = "Mecha", - Military = "Militar", - Mystery = "Misterio", - Music = "Música", - Parody = "Parodia", - Police = "Policía", - Psychological = "Psicológico", - SliceOfLife = "Recuentos de la vida", - Romance = "Romance", - Samurai = "Samurai", - Seinen = "Seinen", - Shoujo = "Shoujo", - Shounen = "Shounen", - Supernatural = "Sobrenatural", - Superpowers = "Superpoderes", - Suspense = "Suspenso", - Horror = "Terror", - Vampires = "Vampiros", - Yaoi = "Yaoi", - Yuri = "Yuri", - } \ No newline at end of file + Action = "Acción", + MartialArts = "Artes Marciales", + Adventure = "Aventuras", + Racing = "Carreras", + ScienceFiction = "Ciencia Ficción", + Comedy = "Comedia", + Dementia = "Demencia", + Demons = "Demonios", + Sports = "Deportes", + Drama = "Drama", + Ecchi = "Ecchi", + School = "Escolares", + Space = "Espacial", + Fantasy = "Fantasía", + Harem = "Harem", + Historical = "Histórico", + Kids = "Infantil", + Josei = "Josei", + Games = "Juegos", + Magic = "Magia", + Mecha = "Mecha", + Military = "Militar", + Mystery = "Misterio", + Music = "Música", + Parody = "Parodia", + Police = "Policía", + Psychological = "Psicológico", + SliceOfLife = "Recuentos de la vida", + Romance = "Romance", + Samurai = "Samurai", + Seinen = "Seinen", + Shoujo = "Shoujo", + Shounen = "Shounen", + Supernatural = "Sobrenatural", + Superpowers = "Superpoderes", + Suspense = "Suspenso", + Horror = "Terror", + Vampires = "Vampiros", + Yaoi = "Yaoi", + Yuri = "Yuri", +} diff --git a/src/types/image.ts b/src/types/image.ts index 4e1e0897..6fe53d17 100644 --- a/src/types/image.ts +++ b/src/types/image.ts @@ -1,36 +1,36 @@ -//Spanish Providers - TypeScript version - -/** - * In most animes include a image and banner that these anime - * banner probably it isn't common into pages, so banner property - * is opcional, the anime's image or cover always display in the pages. - * - * @author Mawfyy - */ -export interface IImage { - /** The URL of the content image */ - url: string; - /** - * The URL of the content banner. It is optional because it is not available - * in all sites. */ - banner?: string; -} - -/** - * In most animes include a image and banner that these anime - * banner probably it isn't common into pages, so banner property - * is opcional, the anime's image or cover always display in the pages. - * - * @author Mawfyy - */ -export class Image implements IImage { - /** @inheritdoc */ - url: string; - /** @inheritdoc */ - banner?: string; - - constructor(url: string, banner?: string) { - this.url = url; - this.banner = banner; - } -} +//Spanish Providers - TypeScript version + +/** + * In most animes include a image and banner that these anime + * banner probably it isn't common into pages, so banner property + * is opcional, the anime's image or cover always display in the pages. + * + * @author Mawfyy + */ +export interface IImage { + /** The URL of the content image */ + url: string; + /** + * The URL of the content banner. It is optional because it is not available + * in all sites. */ + banner?: string; +} + +/** + * In most animes include a image and banner that these anime + * banner probably it isn't common into pages, so banner property + * is opcional, the anime's image or cover always display in the pages. + * + * @author Mawfyy + */ +export class Image implements IImage { + /** @inheritdoc */ + url: string; + /** @inheritdoc */ + banner?: string; + + constructor(url: string, banner?: string) { + this.url = url; + this.banner = banner; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 5059f75d..16a4aa6c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,4 @@ -export * from "./anime" -export * from "./episode" -export * from "./image" -export * from "./date" \ No newline at end of file +export * from "./anime"; +export * from "./episode"; +export * from "./image"; +export * from "./date"; diff --git a/src/types/manga.ts b/src/types/manga.ts index 6b9b0d1e..529a5316 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -135,7 +135,7 @@ export interface IMangaResult { thumbnail?: IImage; /** {@inheritdoc IManga.url} */ url: `/manga/${string}/title/${string}`; -}//filter +} //filter /** * This class defines the basic properties that a manga website can @@ -229,5 +229,5 @@ export class MangaVolume implements IMangaVolume { /** @inheritdoc */ thumbnail?: string; /** @inheritdoc */ - url?:`/manga/${string}/volume/${string}`; // title or number + url?: `/manga/${string}/volume/${string}`; // title or number } diff --git a/src/types/movie.d.ts b/src/types/movie.d.ts index 78baee26..021e79a1 100644 --- a/src/types/movie.d.ts +++ b/src/types/movie.d.ts @@ -1,36 +1,31 @@ - /* * - *Specifies fields that has a movie, as rating or genres + *Specifies fields that has a movie, as rating or genres * to that movie. - * + * * @author Mawfyy */ export interface IMovie { - title?: String, - originalTitle: String, - dateReleased: String, - durationInSeconds: number | String, - description?: String, - url: String, - urlTrailer?: String - genre?: String[] - rating?: number, - animeNameRelated?: String + title?: String; + originalTitle: String; + dateReleased: String; + durationInSeconds: number | String; + description?: String; + url: String; + urlTrailer?: String; + genre?: String[]; + rating?: number; + animeNameRelated?: String; cast?: { - directors?: String[] - writes?: String[] - actors?: String[] - } + directors?: String[]; + writes?: String[]; + actors?: String[]; + }; } - export class Movie { - movie: IMovie; - getMovie(): IMovie - + getMovie(): IMovie; } - diff --git a/src/types/search.ts b/src/types/search.ts index 59ed7258..035e465c 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,87 +1,87 @@ -//Spanish Providers - TypeScript version - -/** - * Anime search helpers, use them with you scrapping by filter (searching..), - * this format help you how you can return - * theses results - * - * @author Mawfyy - * @author Zukaritasu - */ -export interface IAnimeSearch { - /** Name of the anime that was the result of your search */ - name: string; - /** The URL of the anime image */ - image: string; - /** The anime URL from the API */ - url: `/anime/${string}/name/${string}` | string; // API url - /** - * Defines the type of content to which the anime is directed, which - * can be a movie, OVA, ONA, etc... */ - type?: string; -} - -/** - * To navigate more easily among the infinite number of results that the - * API can return, we use this interface in which there is information - * about which page is being searched and how many pages are still available. - * - * @author Zukaritasu - */ -export interface IPageNavigation { - /** number of pages available to search */ - count?: number; - /** page number where you are currently located */ - current?: number; - /** the next page number */ - next?: number; - /** Indicates if there is a next page available */ - hasNext?: boolean -} - -/** - * Search results including information on the page number of the - * searched web site - * - * @author Zukaritasu - */ -export interface IResultSearch { - /** Search by navigation */ - nav?: IPageNavigation; - /** A list of the results obtained */ - results: T[]; -} - -/** - * Anime search helpers, use them with you scrapping by filter (searching..), - * this format help you how you can return - * theses results - * - * @author Mawfyy - * @author Zukaritasu - */ -export class AnimeSearch implements IAnimeSearch { - /** @inheritdoc */ - name: string; - /** @inheritdoc */ - image: string; - /** @inheritdoc */ - url: `/anime/${string}/name/${string}` | string; // API url - /** @inheritdoc */ - type?: string; -} - -/** - * Search results including information on the page number of the - * searched web site - * - * @author Zukaritasu - */ -export class ResultSearch implements IResultSearch { - /** @inheritdoc */ - nav?: IPageNavigation; - /** @inheritdoc */ - results: T[] = []; -} - -/** end */ +//Spanish Providers - TypeScript version + +/** + * Anime search helpers, use them with you scrapping by filter (searching..), + * this format help you how you can return + * theses results + * + * @author Mawfyy + * @author Zukaritasu + */ +export interface IAnimeSearch { + /** Name of the anime that was the result of your search */ + name: string; + /** The URL of the anime image */ + image: string; + /** The anime URL from the API */ + url: `/anime/${string}/name/${string}` | string; // API url + /** + * Defines the type of content to which the anime is directed, which + * can be a movie, OVA, ONA, etc... */ + type?: string; +} + +/** + * To navigate more easily among the infinite number of results that the + * API can return, we use this interface in which there is information + * about which page is being searched and how many pages are still available. + * + * @author Zukaritasu + */ +export interface IPageNavigation { + /** number of pages available to search */ + count?: number; + /** page number where you are currently located */ + current?: number; + /** the next page number */ + next?: number; + /** Indicates if there is a next page available */ + hasNext?: boolean; +} + +/** + * Search results including information on the page number of the + * searched web site + * + * @author Zukaritasu + */ +export interface IResultSearch { + /** Search by navigation */ + nav?: IPageNavigation; + /** A list of the results obtained */ + results: T[]; +} + +/** + * Anime search helpers, use them with you scrapping by filter (searching..), + * this format help you how you can return + * theses results + * + * @author Mawfyy + * @author Zukaritasu + */ +export class AnimeSearch implements IAnimeSearch { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + image: string; + /** @inheritdoc */ + url: `/anime/${string}/name/${string}` | string; // API url + /** @inheritdoc */ + type?: string; +} + +/** + * Search results including information on the page number of the + * searched web site + * + * @author Zukaritasu + */ +export class ResultSearch implements IResultSearch { + /** @inheritdoc */ + nav?: IPageNavigation; + /** @inheritdoc */ + results: T[] = []; +} + +/** end */ diff --git a/src/types/utils.ts b/src/types/utils.ts index a9a47a90..3b9d5047 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -3,66 +3,71 @@ import { unpack } from "unpacker"; //Spanish Providers - TypeScript version interface IPageInfo { - name: string - url: string, // url page - server: string, - domain: string + name: string; + url: string; // url page + server: string; + domain: string; } //================== API functions ================== export const api = { - /** - * Replaces the original URL with the API URL - * - * @param info - * @param url - * @returns - */ - getEpisodeURL(info: IPageInfo, url: string): string { - return url.replace(`https://${info.domain}/ver/`, `/anime/${info.name}/episode/`); - }, + /** + * Replaces the original URL with the API URL + * + * @param info + * @param url + * @returns + */ + getEpisodeURL(info: IPageInfo, url: string): string { + return url.replace( + `https://${info.domain}/ver/`, + `/anime/${info.name}/episode/`, + ); + }, - /** - * Replaces the original URL with the API URL - * - * @param info - * @param url - * @returns - */ - getAnimeURL(info: IPageInfo, url: string): string { - return url.replace(`https://${info.domain}/anime/`, `/anime/${info.name}/name/`); - } -} + /** + * Replaces the original URL with the API URL + * + * @param info + * @param url + * @returns + */ + getAnimeURL(info: IPageInfo, url: string): string { + return url.replace( + `https://${info.domain}/anime/`, + `/anime/${info.name}/name/`, + ); + }, +}; //=================================================== export const utils = { - /** - * - * @param object - * @returns - */ - isUsableValue(object: any): boolean { - return object != null && object != undefined; - } -} - + /** + * + * @param object + * @returns + */ + isUsableValue(object: any): boolean { + return object != null && object != undefined; + }, +}; /** - * + * * @param packedString in Base64 - * + * */ export const UnPacked = (packedString: string) => { - let valuePacked: string; + let valuePacked: string; - if (typeof atob === "undefined") { - valuePacked = Buffer.from(packedString, "base64").toString("binary"); - } else { - valuePacked = atob(packedString); - } - console.log(unpack(valuePacked)) - return unpack(valuePacked); -} + if (typeof atob === "undefined") { + valuePacked = Buffer.from(packedString, "base64").toString("binary"); + } else { + valuePacked = atob(packedString); + } + console.log(unpack(valuePacked)); + return unpack(valuePacked); +}; diff --git a/src/utils/manga/schemaProviders.js b/src/utils/manga/schemaProviders.js index 25bd2734..5b9914ac 100644 --- a/src/utils/manga/schemaProviders.js +++ b/src/utils/manga/schemaProviders.js @@ -1,53 +1,53 @@ -import { Image } from '../schemaProviders.js'; +import { Image } from "../schemaProviders.js"; /** - * + * */ export class ChapterView { - title; - url; - image; - manga; + title; + url; + image; + manga; } /** * Describes a manga chapter with its basic information including the * title and an array containing the images or pages of the chapter. - * + * * @author Zukaritasu * @see {Manga} */ export class Chapter { - /** - * The title of the manga chapter. This may contain the chapter number. - * @type {Image} - */ - title; - /** - * The manga chapter number. The value can be expressed in string or - * number if possible. - * @type {(string | number)} - * @default 0 - */ - number = 0; - /** - * The URL of the manga chapter - * @type {string} - */ - url; - /** - * Array containing the URLs of the images or pages of the manga chapter. - * It can also contain the cover - * @type {string[]} - */ - images = []; - /** - * The cover of the manga chapter. All chapters contain a cover being - * the first page or image. It can be extracted from the first element - * of the array images - * @type {string} - */ - cover; + /** + * The title of the manga chapter. This may contain the chapter number. + * @type {Image} + */ + title; + /** + * The manga chapter number. The value can be expressed in string or + * number if possible. + * @type {(string | number)} + * @default 0 + */ + number = 0; + /** + * The URL of the manga chapter + * @type {string} + */ + url; + /** + * Array containing the URLs of the images or pages of the manga chapter. + * It can also contain the cover + * @type {string[]} + */ + images = []; + /** + * The cover of the manga chapter. All chapters contain a cover being + * the first page or image. It can be extracted from the first element + * of the array images + * @type {string} + */ + cover; } /** @@ -55,48 +55,48 @@ export class Chapter { * most of the manga provider pages do not describe volumes or arcs, so * the 'chapters' property contains all the chapters without the need to * separate them by volumes or arcs. - * + * * @author Zukaritasu * @see {Chapter} * @see '../schemaProviders.js#Image' */ export class Manga { - /** - * The title of the manga - * @type {string} - */ - title; - /** - * The URL of the manga - * @type {string} - */ - url; - /** - * The cover of the manga. It can also be the banner and if that is the - * case use the Image class of the anime providers schema - * @type {Image} - * @see '../schemaProviders.js#Image' - */ - image; - /** - * The year of publication of the manga - * @type {number} - */ - year; - /** - * Description or synopsis of the manga - * @type {string} - */ - synopsis; - /** - * Manga genres - * @type {string[]} - */ - genres = []; - /** - * Manga chapters. Most of the pages of manga do not define volumes - * and arcs, so all the chapters are in a single array - * @type {(Chapter[] | string[])} - */ - chapters = []; -} \ No newline at end of file + /** + * The title of the manga + * @type {string} + */ + title; + /** + * The URL of the manga + * @type {string} + */ + url; + /** + * The cover of the manga. It can also be the banner and if that is the + * case use the Image class of the anime providers schema + * @type {Image} + * @see '../schemaProviders.js#Image' + */ + image; + /** + * The year of publication of the manga + * @type {number} + */ + year; + /** + * Description or synopsis of the manga + * @type {string} + */ + synopsis; + /** + * Manga genres + * @type {string[]} + */ + genres = []; + /** + * Manga chapters. Most of the pages of manga do not define volumes + * and arcs, so all the chapters are in a single array + * @type {(Chapter[] | string[])} + */ + chapters = []; +} diff --git a/src/utils/schemaProviders.js b/src/utils/schemaProviders.js index 84b08a4c..bfd3a12f 100644 --- a/src/utils/schemaProviders.js +++ b/src/utils/schemaProviders.js @@ -56,7 +56,7 @@ export class EpisodeServer { */ url; /** - * + * * @param {string} name server name * @param {string} url server url */ @@ -108,9 +108,9 @@ export class Episode { * @returns Episode number */ static getEpisodeNumber(name) { - if (typeof name === 'string') { + if (typeof name === "string") { for (let i = name.length - 1; i >= 0; i--) { - if (name[i] === ' ') { + if (name[i] === " ") { return parseInt(name.substring(i, name.length).trim()); } } @@ -129,11 +129,11 @@ export class Episode { * @enum {String} */ export const ClimaticStation = { - Summer: Symbol('summer'), - Autumn: Symbol('autumn'), - Winter: Symbol('winter'), - Spring: Symbol('spring'), -} + Summer: Symbol("summer"), + Autumn: Symbol("autumn"), + Winter: Symbol("winter"), + Spring: Symbol("spring"), +}; /** * Anime chronology @@ -224,7 +224,7 @@ export class Anime { * Spanish depending on the location * @type {string[]} */ - genres = [] + genres = []; /** * Climatic station from the anime. If the station is not defined then * the default value is null @@ -286,10 +286,10 @@ export class AnimeSearch { * @param {string} type anime type */ constructor(name, image, url, type = null) { - this.name = name; - this.image = image; - this.url = url; - this.type = type; + this.name = name; + this.image = image; + this.url = url; + this.type = type; } } @@ -314,7 +314,7 @@ export class SearchArray { * @type {(string | number)} page the search page number */ constructor(page) { - this.data = new Array(); - this.page = page + this.data = new Array(); + this.page = page; } } diff --git a/src/utils/shemaNewsProviders.js b/src/utils/shemaNewsProviders.js index 55eb4779..0ba6e698 100644 --- a/src/utils/shemaNewsProviders.js +++ b/src/utils/shemaNewsProviders.js @@ -1,39 +1,39 @@ -export class NewsShema{ - constructor(){ - this.data = new Array() - } +export class NewsShema { + constructor() { + this.data = new Array(); + } } -export class NewsInfo{ - /** - * - * @param {*} title - * @param {*} url - * @param {*} uploadedAt - * @param {*} uploadedBy - * @param {*} banner - * @param {*} intro - * @param {*} full - */ - constructor(title, url, uploadedAt, uploadedBy, banner, intro, full){ - this.title = title; - //this.url = url; - this.uploadedAt = uploadedAt; - this.uploadedBy = uploadedBy; - this.topics = new Array(); - this.banner = banner; - this.preview = { - images: new Array(), - full: full, - } - } +export class NewsInfo { + /** + * + * @param {*} title + * @param {*} url + * @param {*} uploadedAt + * @param {*} uploadedBy + * @param {*} banner + * @param {*} intro + * @param {*} full + */ + constructor(title, url, uploadedAt, uploadedBy, banner, intro, full) { + this.title = title; + //this.url = url; + this.uploadedAt = uploadedAt; + this.uploadedBy = uploadedBy; + this.topics = new Array(); + this.banner = banner; + this.preview = { + images: new Array(), + full: full, + }; + } } -export class Post{ - constructor(title, image, date, url){ - this.title = title; - this.image = image; - this.date = date; - this.url = url - this.topics = new Array(); - } -} \ No newline at end of file +export class Post { + constructor(title, image, date, url) { + this.title = title; + this.image = image; + this.date = date; + this.url = url; + this.topics = new Array(); + } +} diff --git a/src/utils/shemaProvidersExperimental.js b/src/utils/shemaProvidersExperimental.js index 15d673cf..973957a6 100644 --- a/src/utils/shemaProvidersExperimental.js +++ b/src/utils/shemaProvidersExperimental.js @@ -5,26 +5,26 @@ /* Search */ export class AnimeSearch { - /** - * - * @param {*} anime_title - * @param {*} anime_image - * @param {*} link_anime - * @param {*} type - * @param {*} current_page - */ - constructor(anime_title, anime_image, anime_link, type) { - this.anime_title = anime_title; - this.anime_image = anime_image; - this.anime_link = anime_link; - this.type = type; - } + /** + * + * @param {*} anime_title + * @param {*} anime_image + * @param {*} link_anime + * @param {*} type + * @param {*} current_page + */ + constructor(anime_title, anime_image, anime_link, type) { + this.anime_title = anime_title; + this.anime_image = anime_image; + this.anime_link = anime_link; + this.type = type; + } } export class SearchArray { - constructor(page) { - this.data = new Array(); - this.page = page; - } + constructor(page) { + this.data = new Array(); + this.page = page; + } } /* Search */ @@ -32,54 +32,54 @@ export class SearchArray { /* Anime Info */ export class GetAnimeEpisodeList { - /** - * @param {*} episode_title String() - * @param {*} episode_number String() - * @param {*} episode_image String() - * @param {*} episode_link String() - */ - constructor(episode_title, episode_number, episode_image, episode_link) { - this.episode_title = episode_title; - this.episode_number = episode_number; - this.episode_image = episode_image; - this.episode_link = episode_link; - } + /** + * @param {*} episode_title String() + * @param {*} episode_number String() + * @param {*} episode_image String() + * @param {*} episode_link String() + */ + constructor(episode_title, episode_number, episode_image, episode_link) { + this.episode_title = episode_title; + this.episode_number = episode_number; + this.episode_image = episode_image; + this.episode_link = episode_link; + } } export class GetAnimeInfo { - /** - * @param {*} anime_title String() - * @param {*} alternative_title String() - * @param {*} description String() - * @param {*} keywords new Array() - * @param {*} status String() - * @param {*} link String() /anime/provider/ - * @param {*} episode_title String() - * @param {*} episode_number String() - * @param {*} image_espisode String() - * @param {*} type String() - * @param {*} anime_image String() - * @param {*} premiere String() - * @author yako - * @description please use: episode_title: String(), episode_number: String(), image_episode: String(), link_episode: String() - */ - constructor(type = null, anime_image = null, premiere = null) { - this.title = String(); - this.alternative_title = new Array(); - this.type = type; - this.image = new String(); - this.synopsis = [ - { - description: String(), - keywords: new Array(), - status: String(), - premiere: premiere, - chronology: [], - }, - ]; - this.anime_similar = new Array(); - this.episode_list = new Array(); - } + /** + * @param {*} anime_title String() + * @param {*} alternative_title String() + * @param {*} description String() + * @param {*} keywords new Array() + * @param {*} status String() + * @param {*} link String() /anime/provider/ + * @param {*} episode_title String() + * @param {*} episode_number String() + * @param {*} image_espisode String() + * @param {*} type String() + * @param {*} anime_image String() + * @param {*} premiere String() + * @author yako + * @description please use: episode_title: String(), episode_number: String(), image_episode: String(), link_episode: String() + */ + constructor(type = null, anime_image = null, premiere = null) { + this.title = String(); + this.alternative_title = new Array(); + this.type = type; + this.image = new String(); + this.synopsis = [ + { + description: String(), + keywords: new Array(), + status: String(), + premiere: premiere, + chronology: [], + }, + ]; + this.anime_similar = new Array(); + this.episode_list = new Array(); + } } /* Anime Info */ @@ -87,32 +87,32 @@ export class GetAnimeInfo { /* Anime Servers */ export class GetAnimeEpisode { - /** - * @param {*} title episode - * @param {*} next episode - * @param {*} previous episode - * @param {*} list episode - */ - constructor(episode_title,episode_next, episode_prev,episode_list) { - this.episode_title = episode_title; - this.episode_next = episode_next; - this.episode_prev = episode_prev; - this.episode_list = episode_list; - this.servers = new Array(); - } + /** + * @param {*} title episode + * @param {*} next episode + * @param {*} previous episode + * @param {*} list episode + */ + constructor(episode_title, episode_next, episode_prev, episode_list) { + this.episode_title = episode_title; + this.episode_next = episode_next; + this.episode_prev = episode_prev; + this.episode_list = episode_list; + this.servers = new Array(); + } } export class GetAnimeServers { - /** - * @param {*} name server - * @param {*} url server - * @param {*} optional is additional information from some providers - */ - constructor(name, url) { - this.name = name; - this.url = url; - this.optional = new Array(); - } + /** + * @param {*} name server + * @param {*} url server + * @param {*} optional is additional information from some providers + */ + constructor(name, url) { + this.name = name; + this.url = url; + this.optional = new Array(); + } } -/* Anime Servers*/ \ No newline at end of file +/* Anime Servers*/ diff --git a/src/utils/utilities.js b/src/utils/utilities.js index 8e9f2f86..3635bcae 100644 --- a/src/utils/utilities.js +++ b/src/utils/utilities.js @@ -1,6 +1,6 @@ export default { - /** Returns true if argument is different from null and undefined */ - isUsableValue: function(value) { - return value != null && value != undefined; - } -} \ No newline at end of file + /** Returns true if argument is different from null and undefined */ + isUsableValue: function (value) { + return value != null && value != undefined; + }, +}; diff --git a/tsconfig.json b/tsconfig.json index 7be0c2ba..32ab9199 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,29 +25,16 @@ "outDir": "./build", "baseUrl": "./src", "paths": { - "@animetypes/*": [ - "./types/*", - ], - "@providers/*":[ + "@animetypes/*": ["./types/*"], + "@providers/*": [ "./scraper/sites/anime/*", "./scraper/sites/manga/*", "./scraper/sites/doramas/*" - - ], - "@routes/manga/*": [ - "./routes/v1/manga/*" - ], - "@routes/anime/*": [ - "./routes/v1/anime/*" - ], - "@routes/doramas/*": [ - "./routes/v1/doramas/*" ], + "@routes/manga/*": ["./routes/v1/manga/*"], + "@routes/anime/*": ["./routes/v1/anime/*"], + "@routes/doramas/*": ["./routes/v1/doramas/*"] } }, - "exclude": [ - "node_modules", - "build", - "**/*.spec.ts" - ] + "exclude": ["node_modules", "build", "**/*.spec.ts"] } diff --git a/vercel.json b/vercel.json index d7de4abd..ba547441 100644 --- a/vercel.json +++ b/vercel.json @@ -12,4 +12,4 @@ "dest": "src/index.ts" } ] -} \ No newline at end of file +} From 3284ffe9ce46b1e51df3bd35eca490ee23aebee3 Mon Sep 17 00:00:00 2001 From: koikiss-dev Date: Sat, 2 Mar 2024 16:50:54 -0600 Subject: [PATCH 03/64] edit --- .../sites/anime/animeBlixs/AnimeBlix.ts | 12 +++++------ src/scraper/sites/anime/animeflv/AnimeFlv.ts | 12 +++++------ .../anime/animelatinohd/AnimeLatinoHD.ts | 8 ++++---- .../sites/anime/animevostfr/Animevostfr.ts | 18 ++++++++--------- .../sites/anime/wcostream/WcoStream.ts | 20 +++++++++---------- src/scraper/sites/anime/zoro/Zoro.ts | 8 ++++---- src/test/Animeflv.spec.ts | 4 ++-- src/test/Zoro.spec.ts | 2 +- 8 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index fdaff2be..7c3930d4 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -8,7 +8,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains @@ -27,7 +27,7 @@ export class AnimeBlix extends AnimeProviderModel { async GetAnimeInfo(anime: string): Promise { try { const { data } = await axios.get( - `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}`, + `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}` ); const $ = cheerio.load(data); @@ -38,7 +38,7 @@ export class AnimeBlix extends AnimeProviderModel { ? $(".cn .info .r .u li span[class='fi']").text() : $(".cn .info .r .u li span[class='es']").text(); const AnimeDate = $( - ".cn .info .r .u li span:contains('Fecha de emisión:')", + ".cn .info .r .u li span:contains('Fecha de emisión:')" ) .next() .text() @@ -106,7 +106,7 @@ export class AnimeBlix extends AnimeProviderModel { const ReplaceSymbols: RegExp = /(,)+/g; const ListEpisode = ListEpisodeIndex.slice( ListEpisodeIndex.indexOf("var eps = "), - ListEpisodeIndex.indexOf(";> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 4c4e3fe2..5cdbae10 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -14,7 +14,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; export class AnimeFlv extends AnimeProviderModel { readonly url = "https://animeflv.ws"; @@ -58,7 +58,7 @@ export class AnimeFlv extends AnimeProviderModel { episode.name = $(e).children(".Title").text().trim(); episode.url = `/anime/flv/episode/${`${l}`.replace( "/anime", - "/anime/flv", + "/anime/flv" )}`; episode.number = $(e).children("p").last().text().trim(); episode.image = $(e).children("figure").find(".lazy").attr("src"); @@ -68,10 +68,10 @@ export class AnimeFlv extends AnimeProviderModel { } catch (error) { console.log( "An error occurred while getting the anime info: invalid name", - error, + error ); throw new Error( - "An error occurred while getting the anime info: invalid name", + "An error occurred while getting the anime info: invalid name" ); } } @@ -83,7 +83,7 @@ export class AnimeFlv extends AnimeProviderModel { status?: StatusAnimeflv, ord?: OrderAnimeflv, page?: number, - title?: string, + title?: string ): Promise> { try { const { data } = await axios.get(`${this.url}/browse`, { @@ -144,7 +144,7 @@ export class AnimeFlv extends AnimeProviderModel { servers.url = videoData; if (videoData.includes("streaming.php")) { await this.getM3U( - `${videoData.replace("streaming.php", "ajax.php")}&refer=none`, + `${videoData.replace("streaming.php", "ajax.php")}&refer=none` ).then((g) => { if (g.source.length) { servers.file_url = g.source[0].file; diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 10b84353..719e5311 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -8,7 +8,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; export class AnimeLatinoHD extends AnimeProviderModel { readonly url = "https://www.animelatinohd.com"; @@ -133,8 +133,8 @@ export class AnimeLatinoHD extends AnimeProviderModel { Server.url = "https://filemoon.sx" + "/e/" + id_file }*/ AnimeEpisodeInfo.servers.push(Server); - }, - ), + } + ) ); return AnimeEpisodeInfo; @@ -148,7 +148,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { type?: number, page?: number, year?: string, - genre?: string, + genre?: string ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index d594368c..9aa9a1c1 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -8,7 +8,7 @@ import { IResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains @@ -27,7 +27,7 @@ export class Animevostfr extends AnimeProviderModel { const $ = cheerio.load(data); const AnimeTypes = $( - ".mvic-info .mvici-right p strong:contains(' Type:')", + ".mvic-info .mvici-right p strong:contains(' Type:')" ) .nextAll() .text(); @@ -44,13 +44,13 @@ export class Animevostfr extends AnimeProviderModel { url: `/anime/animevostfr/name/${anime}`, synopsis: AnimeDescription.slice( AnimeDescription.indexOf("Synopsis:") + "Synopsis:".length, - -1, + -1 ).trim(), alt_name: [ ...AnimeDescription.slice( AnimeDescription.indexOf("Titre alternatif:") + "Titre alternatif:".length, - AnimeDescription.indexOf("Synopsis:"), + AnimeDescription.indexOf("Synopsis:") ) .replace("
\n", "") .split("/") @@ -105,7 +105,7 @@ export class Animevostfr extends AnimeProviderModel { const anime = episode.substring(0, episode.lastIndexOf("-")); const { data } = await axios.get( - `${this.url}/episode/${anime}-episode-${number}`, + `${this.url}/episode/${anime}-episode-${number}` ); const $ = cheerio.load(data); const s = $(".form-group.list-server select option"); @@ -142,7 +142,7 @@ export class Animevostfr extends AnimeProviderModel { await Promise.all( ListServer.map(async (n) => { const servers = await axios.get( - `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId[0]}`, + `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId[0]}` ); let currentData = servers.data; if (n == "opencdn" || n == "photo") { @@ -155,11 +155,11 @@ export class Animevostfr extends AnimeProviderModel { url: currentData, }; AnimeEpisodeInfo.servers.push(Servers); - }), + }) ); AnimeEpisodeInfo.servers.sort( - (a: EpisodeServer, b: EpisodeServer) => a.name.length - b.name.length, + (a: EpisodeServer, b: EpisodeServer) => a.name.length - b.name.length ); return AnimeEpisodeInfo; } catch (error) { @@ -172,7 +172,7 @@ export class Animevostfr extends AnimeProviderModel { type?: number, page?: number, year?: string, - genre?: string, + genre?: string ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index 2b1ba91a..a3fdc50d 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -9,7 +9,7 @@ import { AnimeSearch, } from "../../../../types/search"; import { UnPacked } from "../../../../types/utils"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; /** List of Domains * https://wcostream.tv @@ -50,10 +50,10 @@ export class WcoStream extends AnimeProviderModel { const $ = cheerio.load(data); const image = $( - "#category_description .ui-grid-solo .ui-block-a img", + "#category_description .ui-grid-solo .ui-block-a img" ).attr("src"); const name = $( - ".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x", + ".main .ui-grid-solo.center .ui-block-a > .ui-bar.ui-bar-x" ) .text() .replace("Share On", ""); @@ -95,7 +95,7 @@ export class WcoStream extends AnimeProviderModel { data.includes("English Dubbed") ? "English Dubbed" : "English Subbed", - "", + "" ) .replace("Episode", "") .trim() @@ -144,7 +144,7 @@ export class WcoStream extends AnimeProviderModel { const anime = episode.substring(0, episode.lastIndexOf("-")); const { data } = await axios.get( - `https://www.wcostream.tv/playlist-cat/${anime}`, + `https://www.wcostream.tv/playlist-cat/${anime}` ); const $ = cheerio.load(data); @@ -154,7 +154,7 @@ export class WcoStream extends AnimeProviderModel { .trim() .slice(mainUrl.search("playlist:") + 6, mainUrl.search("image: ") - 4) .trim() - .replace(",", ""), + .replace(",", "") ); const mainData = await axios.get(this.url + mainOrigin); @@ -162,7 +162,7 @@ export class WcoStream extends AnimeProviderModel { mainData.data .replaceAll(":image", " type='image'") .replaceAll(":source", " type='video'") - .trim(), + .trim() ); const AnimeEpisodeInfo: Episode = { @@ -224,7 +224,7 @@ export class WcoStream extends AnimeProviderModel { async GetAnimeByFilter( search?: string, - page?: number, + page?: number ): Promise> { try { const formdata = new FormData(); @@ -278,8 +278,8 @@ export class WcoStream extends AnimeProviderModel { const RequestBR = await eval( UnBuffer.slice( UnBuffer.indexOf("{sources:[{file:") + "{sources:[{file:".length, - UnBuffer.indexOf("}],image:", 1), - ), + UnBuffer.indexOf("}],image:", 1) + ) ); return RequestBR; diff --git a/src/scraper/sites/anime/zoro/Zoro.ts b/src/scraper/sites/anime/zoro/Zoro.ts index 517db05e..df00ab3c 100644 --- a/src/scraper/sites/anime/zoro/Zoro.ts +++ b/src/scraper/sites/anime/zoro/Zoro.ts @@ -7,7 +7,7 @@ import { ResultSearch, IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "scraper/ScraperAnimeModel"; +import { AnimeProviderModel } from "../../../ScraperAnimeModel"; export class Zoro extends AnimeProviderModel { readonly url = "https://aniwatch.to"; @@ -71,7 +71,7 @@ export class Zoro extends AnimeProviderModel { language?: string, sort?: string, genres?: string, - page_anime?: string, + page_anime?: string ) { try { const { data } = await axios.get(`${this.url}/filter`, { @@ -125,7 +125,7 @@ export class Zoro extends AnimeProviderModel { Referer: `https://zoro.to/watch/${animename + "-" + ep}`, "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", }, - }, + } ); const $ = load(data.html); const epi = new Episode(); @@ -172,7 +172,7 @@ export class Zoro extends AnimeProviderModel { private async getServers(id): Promise { const { data } = await axios.get( - `${this.url}/ajax/v2/episode/sources?id=${id}`, + `${this.url}/ajax/v2/episode/sources?id=${id}` ); return data; } diff --git a/src/test/Animeflv.spec.ts b/src/test/Animeflv.spec.ts index ef21cf18..42196614 100644 --- a/src/test/Animeflv.spec.ts +++ b/src/test/Animeflv.spec.ts @@ -23,13 +23,13 @@ describe("AnimeFlv", () => { }); it("should filter anime successfully", async () => { - const result = await animeFlv.Filter( + const result = await animeFlv.GetAnimeByFilter( Genres.Action, "all", "all", StatusAnimeflv.OnGoing, 1, - 1, + 1 ); expect(result.results.length).toBeGreaterThan(0); }); diff --git a/src/test/Zoro.spec.ts b/src/test/Zoro.spec.ts index e4f9ab4c..e2ad3665 100644 --- a/src/test/Zoro.spec.ts +++ b/src/test/Zoro.spec.ts @@ -16,7 +16,7 @@ describe("Zoro", () => { expect(animeInfo.genres.length).toBeGreaterThan(0); }); it("should filter anime successfully", async () => { - const result = await zoro.Filter("2"); + const result = await zoro.GetAnimeByFilter("2"); expect(result.results.length).toBeGreaterThan(0); }); }); From 9a3bd680dca9afef669cea6551d2fe07b12cb408 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Thu, 21 Mar 2024 18:07:45 -0400 Subject: [PATCH 04/64] The getAnime function caused slowness when searching for an anime --- src/scraper/sites/anime/monoschinos/Monoschinos.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index 30f28b50..c6dcf7e1 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -160,7 +160,14 @@ async function getLastAnimes(url?: string): Promise { for (let i = 0; i < elements.length; i++) { const href = $(elements[i]).find('a').attr('href'); if (utils.isUsableValue(href) && href !== 'https://monoschinos2.com/emision?p=2') { - animes.push(await getAnime(href)); + //animes.push(await getAnime(href)); + + let anime = new types.Anime(); + anime.url = $(elements[i]).find('a').attr('href'); + anime.image = new types.Image($(elements[i]).find('a img').attr('src')); + anime.name = $(elements[i]).find('h3.seristitles').text(); + + animes.push(anime); } } return animes; From 425a7414d0aec47293f98f77dd013de58a249d74 Mon Sep 17 00:00:00 2001 From: koikiss-dev Date: Thu, 21 Mar 2024 21:54:09 -0600 Subject: [PATCH 05/64] add "type" into imports --- src/scraper/ScraperAnimeModel.ts | 2 +- .../sites/anime/animeBlixs/AnimeBlix.ts | 4 +- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 4 +- .../anime/animelatinohd/AnimeLatinoHD.ts | 4 +- .../sites/anime/animevostfr/Animevostfr.ts | 4 +- .../sites/anime/gogoanime/Gogoanime.ts | 223 ++++++++---------- .../sites/anime/monoschinos/Monoschinos.ts | 20 +- src/scraper/sites/anime/tioanime/TioAnime.ts | 35 ++- .../sites/anime/wcostream/WcoStream.ts | 4 +- src/scraper/sites/anime/zoro/Zoro.ts | 2 +- .../sites/doramas/dramanice/Dramanice.ts | 10 +- .../sites/manga/MangaBuddy/MangaBuddy.ts | 12 +- .../sites/manga/MangaReader/MangaReader.ts | 32 +-- src/scraper/sites/manga/comick/Comick.ts | 30 ++- src/scraper/sites/manga/inmanga/Inmanga.ts | 26 +- .../sites/manga/manganelo/Manganelo.ts | 14 +- .../manganelo/managers/ManganatoURLManager.ts | 4 +- src/scraper/sites/manga/nhentai/Nhentai.ts | 6 +- 18 files changed, 207 insertions(+), 229 deletions(-) diff --git a/src/scraper/ScraperAnimeModel.ts b/src/scraper/ScraperAnimeModel.ts index bdaefff0..0b9dda79 100644 --- a/src/scraper/ScraperAnimeModel.ts +++ b/src/scraper/ScraperAnimeModel.ts @@ -1,5 +1,5 @@ import { Anime } from "../types/anime"; -import { IResultSearch, IAnimeSearch } from "../types/search"; +import { type IResultSearch, type IAnimeSearch } from "../types/search"; import { Episode } from "../types/episode"; export abstract class AnimeProviderModel { diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index 7c3930d4..83aa1a55 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -5,8 +5,8 @@ import { Episode, EpisodeServer } from "../../../../types/episode"; import { AnimeSearch, ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 5cdbae10..5c43387c 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -11,8 +11,8 @@ import { import { AnimeSearch, ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 719e5311..cc5ad4cc 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -5,8 +5,8 @@ import { Episode, EpisodeServer } from "../../../../types/episode"; import { AnimeSearch, ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index 9aa9a1c1..c9334a01 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -5,8 +5,8 @@ import { Episode, EpisodeServer } from "../../../../types/episode"; import { AnimeSearch, ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; //import { Calendar } from "@animetypes/date"; diff --git a/src/scraper/sites/anime/gogoanime/Gogoanime.ts b/src/scraper/sites/anime/gogoanime/Gogoanime.ts index 301d40da..70cae333 100644 --- a/src/scraper/sites/anime/gogoanime/Gogoanime.ts +++ b/src/scraper/sites/anime/gogoanime/Gogoanime.ts @@ -1,181 +1,148 @@ -/* import { getHTML } from "./assets/getHTML"; +import { getHTML } from "./assets/getHTML"; import { Anime } from "../../../../types/anime"; import { getAllAnimes } from "./assets/getAllAnimesHTML"; import { Episode } from "../../../../types/episode"; - //This is a class export class GogoanimeInfo { - async getAnimeInfo(animeName: string) { - - try { - - const $ = await getHTML(`https://www3.gogoanimes.fi/category/${animeName}`); - - const anime = new Anime; - - anime.genres = []; - - anime.name = $("div.anime_info_body_bg h1").text(); - - anime.image = { - url: $("div.anime_info_body_bg ").find("img").attr("src") - } - - anime.alt_name = $("div.anime_info_body_bg"). - find("p"). - last(). - text(). - replace("Other name: ", ""). - trim(); - - - - $("div.anime_info_body_bg p.type a").each((iterator, elementHTML) => { - - if (iterator) - - anime.genres.push($(elementHTML).html()); - - }) - - - $('div.anime_info_body_bg p.type').each((index, element) => { - //Skips for first p.type - if (index) - - if (index == 1) { - anime.synopsis = $(element).text().replace('Plot Summary: ', '').trim(); - } - - if (index == 4 && $(element).text().trim() != 'Status: ') { + try { + const $ = await getHTML( + `https://www3.gogoanimes.fi/category/${animeName}` + ); + + const anime = new Anime(); + + anime.genres = []; + + anime.name = $("div.anime_info_body_bg h1").text(); + + anime.image = { + url: $("div.anime_info_body_bg ").find("img").attr("src"), + }; + + anime.alt_name = $("div.anime_info_body_bg") + .find("p") + .last() + .text() + .replace("Other name: ", "") + .trim(); + + $("div.anime_info_body_bg p.type a").each((iterator, elementHTML) => { + if (iterator) anime.genres.push($(elementHTML).html()); + }); + + $("div.anime_info_body_bg p.type").each((index, element) => { + //Skips for first p.type + if (index) + if (index == 1) { + anime.synopsis = $(element) + .text() + .replace("Plot Summary: ", "") + .trim(); + } + + if (index == 4 && $(element).text().trim() != "Status: ") { anime.status = true; - } - - if (index == 5) { - anime.alt_name = $(element).text().trim() - .replace('Other name:', '') - .replace(/\s/g, '') } - }) - + if (index == 5) { + anime.alt_name = $(element) + .text() + .trim() + .replace("Other name:", "") + .replace(/\s/g, ""); + } + }); - let getNumberEpisodes: any = $('#episode_page li').last().text().trim().split("-")[1]; - getNumberEpisodes = parseInt(getNumberEpisodes); + let getNumberEpisodes: any = $("#episode_page li") + .last() + .text() + .trim() + .split("-")[1]; + getNumberEpisodes = parseInt(getNumberEpisodes); - for (let index = 1; index <= getNumberEpisodes; index++) { - anime.episodes.push({ + for (let index = 1; index <= getNumberEpisodes; index++) { + anime.episodes.push({ name: `${animeName}-cap-${index}`, - url: `/anime/gogoanime/episode/${animeName}/${index}`,//sorry for the change + url: `/anime/gogoanime/episode/${animeName}/${index}`, //sorry for the change number: `${index}`, - image: "That isn't image" - }) - } - - return anime + image: "That isn't image", + }); + } - } catch(error) { + return anime; + } catch (error) { return error; } - - } - } export class GogoanimeFilter { + async getAnimesfilterByGenre(genre: string, numPage: number) { + let animesByGenre = await getAllAnimes( + `https://www3.gogoanimes.fi/genre/${genre}`, + numPage + ); - async getAnimesfilterByGenre(genre: string, numPage: number) { - - - let animesByGenre = await getAllAnimes( - - `https://www3.gogoanimes.fi/genre/${genre}`, numPage - - ) - - - return animesByGenre; - - } - - - async filterBySeasons( season: string, year: string, numPage: number ) { + return animesByGenre; + } + async filterBySeasons(season: string, year: string, numPage: number) { let animes = await getAllAnimes( - `https://www3.gogoanimes.fi/sub-category/${season}-${year}-anime`, numPage + `https://www3.gogoanimes.fi/sub-category/${season}-${year}-anime`, + numPage ); - - return animes - + return animes; } - } - - - export class GogoanimeServer { - - async getAnimeServerEpisode(animeName: string, episodeNumber: number) { - - + async getAnimeServerEpisode(animeName: string, episodeNumber: number) { let serverUrl: string; let serverName: string; const $ = await getHTML( - `https://www3.gogoanimes.fi/${animeName}-episode-${episodeNumber}` + `https://www3.gogoanimes.fi/${animeName}-episode-${episodeNumber}` ); const episode = new Episode(); - - episode.name = "This isn't name"; - episode.servers = []; - - - - $(".anime_muti_link ul li ").each((iterator, element) => { + episode.name = "This isn't name"; + episode.servers = []; - if (iterator == 0 || iterator == 1){ - - serverName = $(element).find("a").text(). - replace(" this server", "").trim(); + $(".anime_muti_link ul li ").each((iterator, element) => { + if (iterator == 0 || iterator == 1) { + serverName = $(element) + .find("a") + .text() + .replace(" this server", "") + .trim(); serverUrl = `http:${$(element).find("a").attr("data-video")}`; - - episode.servers.push({ + episode.servers.push({ name: serverName, - url: serverUrl + url: serverUrl, }); - - }if(iterator > 2) { - - - serverName = $(element).find("a").text(). - replace(" this server", "").trim(); - - serverUrl = $(element).find("a").attr("data-video"); + } + if (iterator > 2) { + serverName = $(element) + .find("a") + .text() + .replace(" this server", "") + .trim(); + serverUrl = $(element).find("a").attr("data-video"); - episode.servers.push({ + episode.servers.push({ name: serverName, - url: serverUrl + url: serverUrl, }); - } - - }) + }); return episode; } - } - - - - */ diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index 6acbd4a5..935d13ff 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -4,8 +4,8 @@ import { api, utils } from "../../../../types/utils"; import * as types from "../../../../types/."; import { ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; const PageInfo = { @@ -32,9 +32,9 @@ async function getEpisodeServers(url: string): Promise { new types.EpisodeServer( $(element).text().trim(), Buffer.from($(element).attr("data-player"), "base64").toString( - "binary", - ), - ), + "binary" + ) + ) ); }); return servers; @@ -103,7 +103,7 @@ function getAnimeEpisodes($): types.Episode[] { episode.name = $(element).find("img.animeimghv").attr("alt"); episode.url = api.getEpisodeURL( PageInfo, - $(element).find("a").attr("href"), + $(element).find("a").attr("href") ); episodes.push(episode); }); @@ -157,7 +157,7 @@ async function getAnime(url: string): Promise { // The anime page in monoschinos does not define the chronology and type const $ = cheerio.load((await axios.get(url)).data); const calendar = getAnimeCalendar( - $($("div.chapterdetails nav").children()[1]), + $($("div.chapterdetails nav").children()[1]) ); const anime = new types.Anime(); anime.name = $("div.chapterdetails").find("h1").text(); @@ -167,7 +167,7 @@ async function getAnime(url: string): Promise { anime.genres = getGenres($); anime.image = new types.Image( $("div.chapterpic img").attr("src"), - $("div.herobg img").attr("src"), + $("div.herobg img").attr("src") ); anime.status = "estreno" === $("div.butns button.btn1").text().toLowerCase().trim(); @@ -186,7 +186,7 @@ async function getAnime(url: string): Promise { async function getLastAnimes(url?: string): Promise { let animes: types.Anime[] = []; const $ = cheerio.load( - (await axios.get(url ?? `${PageInfo.url}/emision`)).data, + (await axios.get(url ?? `${PageInfo.url}/emision`)).data ); const elements = $("div.heroarea div.heromain div.row").children(); for (let i = 0; i < elements.length; i++) { @@ -221,7 +221,7 @@ export class Monoschinos { category?: string, genre?: string, year?: string, - letter?: string, + letter?: string ): Promise> { const animes = new ResultSearch(); const link = utils.isUsableValue(name) diff --git a/src/scraper/sites/anime/tioanime/TioAnime.ts b/src/scraper/sites/anime/tioanime/TioAnime.ts index 55ce4f81..dd0e14ba 100644 --- a/src/scraper/sites/anime/tioanime/TioAnime.ts +++ b/src/scraper/sites/anime/tioanime/TioAnime.ts @@ -4,8 +4,8 @@ import { utils } from "../../../../types/utils"; import * as types from "../../../../types/."; import { ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; const PageInfo = { @@ -21,8 +21,8 @@ function getAnimeChronology($) { new types.Chronology( $(element).find("h3.title").text(), PageInfo.url + $(element).find("div.media-body a").attr("href"), - PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src"), - ), + PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src") + ) ); }); return chrono_list; @@ -37,11 +37,11 @@ async function getEpisodeServers(url) { const videos = new Function( script .substring(0, script.indexOf("$(document)")) - .replace("var videos =", "return"), + .replace("var videos =", "return") )(); for (let i = 0; i < videos.length; i++) { servers.push( - new types.EpisodeServer(videos[i][0], videos[i][1].replace("\\", "")), + new types.EpisodeServer(videos[i][0], videos[i][1].replace("\\", "")) ); } @@ -149,7 +149,7 @@ async function getAnime(url) { //anime.url = url; anime.url = url.replace( "https://tioanime.com/anime/", - "/anime/tioanime/name/", + "/anime/tioanime/name/" ); //anime.type = $('div.meta span.anime-type-peli').text(); anime.type = (() => { @@ -171,8 +171,8 @@ async function getAnime(url) { new types.Calendar( data.info.length < 4 ? parseInt($("div.meta span.year").text().trim().substring(0, 4)) - : new Date(data.info[3]).getFullYear(), - ), + : new Date(data.info[3]).getFullYear() + ) ); anime.synopsis = $("p.sinopsis").text().trim(); anime.genres = getGenres($, $("div.container p.genres span")); @@ -180,7 +180,7 @@ async function getAnime(url) { PageInfo.url + $("div.container div.thumb figure img").attr("src"), $("figure.backdrop img").attr("src") == undefined ? "" - : PageInfo.url + $("figure.backdrop img").attr("src"), + : PageInfo.url + $("figure.backdrop img").attr("src") ); anime.status = $("div.thumb a.status").text().trim() === "En emision"; anime.station = $("div.meta span.fa-snowflake").text().trim().split("\n")[0]; @@ -197,7 +197,7 @@ async function getLastAnimes(url: string) { const elements = $( utils.isUsableValue(url) ? "ul.animes" - : "div.container section ul.list-unstyled.row li", + : "div.container section ul.list-unstyled.row li" ).children(); for (let i = 0; i < elements.length; i++) { const anime_url = $(elements[i]).find("article.anime a").attr("href"); @@ -216,15 +216,14 @@ async function getSectionContents(section: number) { let animes: types.IAnime[] = []; try { const $ = cheerio.load( - (await axios.get(`${PageInfo.url}/directorio?type%5B%5D=${section}`)) - .data, + (await axios.get(`${PageInfo.url}/directorio?type%5B%5D=${section}`)).data ); const elements = $(`ul.animes`).children(); for (let i = 0; i < elements.length; i++) { animes.push( await getAnime( - PageInfo.url + $(elements[i]).find("article.anime a").attr("href"), - ), + PageInfo.url + $(elements[i]).find("article.anime a").attr("href") + ) ); } } catch (error) { @@ -284,7 +283,7 @@ export class TioAnime { genres?: string[], year_range?: IYearRange, status?: number, - sort?: string, + sort?: string ): Promise> { const animes = new ResultSearch(); let usable; @@ -298,11 +297,11 @@ export class TioAnime { ? `q=${name}` : `${this.arrayToURLParams("type", types)}${this.arrayToURLParams( "genero", - genres, + genres )}year=${year_range.begin}%2C${year_range.end}&status=${ status ?? 2 }&sort=${sort ?? "recent"}` - }`, + }` ) ).forEach((element) => { if (utils.isUsableValue(element)) { diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index a3fdc50d..730b6cb4 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -3,8 +3,8 @@ import axios from "axios"; import { Anime } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, ResultSearch, AnimeSearch, } from "../../../../types/search"; diff --git a/src/scraper/sites/anime/zoro/Zoro.ts b/src/scraper/sites/anime/zoro/Zoro.ts index df00ab3c..67acb943 100644 --- a/src/scraper/sites/anime/zoro/Zoro.ts +++ b/src/scraper/sites/anime/zoro/Zoro.ts @@ -5,7 +5,7 @@ import { Episode, EpisodeServer } from "../../../../types/episode"; import { AnimeSearch, ResultSearch, - IAnimeSearch, + type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; diff --git a/src/scraper/sites/doramas/dramanice/Dramanice.ts b/src/scraper/sites/doramas/dramanice/Dramanice.ts index a8087c4b..a84b5b42 100644 --- a/src/scraper/sites/doramas/dramanice/Dramanice.ts +++ b/src/scraper/sites/doramas/dramanice/Dramanice.ts @@ -5,8 +5,8 @@ import { Episode, EpisodeServer } from "../../../../types/episode"; import { AnimeSearch, ResultSearch, - IResultSearch, - IAnimeSearch, + type IResultSearch, + type IAnimeSearch, } from "../../../../types/search"; export class Dramanice { @@ -132,8 +132,8 @@ export class Dramanice { Server.url = "https://filemoon.sx" + "/e/" + id_file }*/ AnimeEpisodeInfo.servers.push(Server); - }, - ), + } + ) ); return AnimeEpisodeInfo; @@ -147,7 +147,7 @@ export class Dramanice { type?: number, page?: number, year?: string, - genre?: string, + genre?: string ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { diff --git a/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts b/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts index d04a3b87..feede7df 100644 --- a/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts +++ b/src/scraper/sites/manga/MangaBuddy/MangaBuddy.ts @@ -1,7 +1,11 @@ import axios from "axios"; import { load } from "cheerio"; -import { Manga, IMangaChapter, IMangaResult } from "../../../../types/manga"; -import { IResultSearch } from "@animetypes/search"; +import { + Manga, + type IMangaChapter, + type IMangaResult, +} from "../../../../types/manga"; +import { type IResultSearch } from "@animetypes/search"; export class MangaBuddy { readonly url = "https://mangabuddy.com"; @@ -60,10 +64,10 @@ export class MangaBuddy { const dateText = $(e).find("time.chapter-update").text().trim(); //date string const yearMangaVerification = Number.isNaN( - Number(dateText.split(" ")[2]), + Number(dateText.split(" ")[2]) ); const dayMangaVerification = Number.isNaN( - Number(dateText.split(" ")[0]), + Number(dateText.split(" ")[0]) ); let monthAbbr; diff --git a/src/scraper/sites/manga/MangaReader/MangaReader.ts b/src/scraper/sites/manga/MangaReader/MangaReader.ts index b17c1e61..20ee8963 100644 --- a/src/scraper/sites/manga/MangaReader/MangaReader.ts +++ b/src/scraper/sites/manga/MangaReader/MangaReader.ts @@ -1,6 +1,6 @@ import { Image } from "../../../../types/image"; import { - IMangaResult, + type IMangaResult, Manga, MangaChapter, MangaVolume, @@ -12,7 +12,7 @@ import { MangaReaderChapterType, MangaReaderFilterData, } from "./MangaReaderTypes"; -import { IResultSearch, ResultSearch } from "../../../../types/search"; +import { type IResultSearch, ResultSearch } from "../../../../types/search"; export class MangaReader { readonly url = "https://mangareader.to"; @@ -22,7 +22,7 @@ export class MangaReader { const $ = load(data); const rangeResult: number[] = $( - "div.volume-list-ul div.manga_list div.manga_list-wrap", + "div.volume-list-ul div.manga_list div.manga_list-wrap" ) .find("div.item") .map((_, element) => { @@ -41,7 +41,7 @@ export class MangaReader { mangaId: number, chapterNumber: number, language: (typeof MangaReaderFilterLanguage)[number], - type: MangaReaderChapterType, + type: MangaReaderChapterType ): Promise { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const $ = load(data); @@ -91,11 +91,11 @@ export class MangaReader { else if (type === "volume") idType = "vol"; const { data: pagesAjaxData } = await axios.get( - `${this.url}/ajax/image/list/${idType}/${chapterId}?mode=horizontal&quality=high`, + `${this.url}/ajax/image/list/${idType}/${chapterId}?mode=horizontal&quality=high` ); const $pagesAjaxData = load(pagesAjaxData.html); const pagesSection = $pagesAjaxData( - "div#main-wrapper div.container-reader-hoz div#divslide div.divslide-wrapper div.ds-item", + "div#main-wrapper div.container-reader-hoz div#divslide div.divslide-wrapper div.ds-item" ).find("div.ds-image"); let pages = pagesSection @@ -109,14 +109,14 @@ export class MangaReader { try { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const { data: charactersAjaxList } = await axios.get( - `${this.url}/ajax/character/list/${mangaId}`, + `${this.url}/ajax/character/list/${mangaId}` ); const $ = load(data); const $characterListAjaxResult = load(charactersAjaxList.html); const charactersSection = $characterListAjaxResult( - "div.character-list div.cl-item div.cli-info", + "div.character-list div.cl-item div.cli-info" ); const title = $("h2.manga-name").text().trim(); @@ -124,7 +124,7 @@ export class MangaReader { ? Array.of($("div.manga-name-or").text().trim()) : null; const thumbnailUrl = $("div.manga-poster img.manga-poster-img").attr( - "src", + "src" ); const description = $("div.description").text().trim(); const status = $("div.anisc-info div.item") @@ -196,7 +196,7 @@ export class MangaReader { manga.volumes = []; const mangaVolumeItemSection = $( - "div.volume-list-ul div.manga_list div.manga_list-wrap", + "div.volume-list-ul div.manga_list div.manga_list-wrap" ); let langVolumeCode: string = ``; @@ -243,13 +243,13 @@ export class MangaReader { } catch (error) { console.log(error); throw new Error( - "I've found an error while trying to get the manga info.", + "I've found an error while trying to get the manga info." ); } } async Filter( - options: MangaReaderFilterData, + options: MangaReaderFilterData ): Promise> { const { type, @@ -337,11 +337,11 @@ export class MangaReader { mangaId: number, chapterNumber: number, language: (typeof MangaReaderFilterLanguage)[number], - type: MangaReaderChapterType, + type: MangaReaderChapterType ) { try { const { data } = await axios.get( - `${this.url}/read/a-${mangaId}/${language}/${type}-${chapterNumber}`, + `${this.url}/read/a-${mangaId}/${language}/${type}-${chapterNumber}` ); const $ = load(data); @@ -353,7 +353,7 @@ export class MangaReader { mangaId, chapterNumber, language, - type, + type ); if (type === "chapter") { @@ -384,7 +384,7 @@ export class MangaReader { } catch (error) { console.log(error); throw new Error( - `I've found an error while trying to get the manga ${type} pages.`, + `I've found an error while trying to get the manga ${type} pages.` ); } } diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index fca0ad64..c898807c 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -1,7 +1,11 @@ import * as cheerio from "cheerio"; import axios from "axios"; -import { Manga, MangaChapter, IMangaResult } from "../../../../types/manga"; -import { IResultSearch } from "../../../../types/search"; +import { + Manga, + MangaChapter, + type IMangaResult, +} from "../../../../types/manga"; +import { type IResultSearch } from "../../../../types/search"; //Default Set Axios Cookie axios.defaults.withCredentials = true; @@ -28,7 +32,7 @@ export class Comick { search?: string, type?: number, year?: string, - genre?: string, + genre?: string ) { try { const { data } = await axios.get(`${this.api}/v1.0/search`, { @@ -59,7 +63,7 @@ export class Comick { url: `/manga/comick/title/${e.slug}`, }; ResultList.results.push(ListMangaResult); - }, + } ); return ResultList; @@ -77,14 +81,14 @@ export class Comick { const mangaInfoParseObj = data; const dataApi = await axios.get( - `${this.api}/comic/${mangaInfoParseObj.comic.hid}/chapters${currentLang}`, + `${this.api}/comic/${mangaInfoParseObj.comic.hid}/chapters${currentLang}` ); const MangaInfo: Manga = { id: mangaInfoParseObj.comic.id, title: mangaInfoParseObj.comic.title, altTitles: mangaInfoParseObj.comic.md_titles.map( - (e: { title: string }) => e.title, + (e: { title: string }) => e.title ), url: `/manga/comick/title/${mangaInfoParseObj.comic.slug}`, description: mangaInfoParseObj.comic.desc, @@ -93,7 +97,7 @@ export class Comick { status: mangaInfoParseObj.comic.status == "1" ? "ongoing" : "completed", authors: mangaInfoParseObj.authors.map((e: { name: string }) => e.name), genres: mangaInfoParseObj.comic.md_comic_md_genres.map( - (e: { md_genres: { name: string } }) => e.md_genres.name, + (e: { md_genres: { name: string } }) => e.md_genres.name ), chapters: [], thumbnail: { @@ -131,9 +135,9 @@ export class Comick { }, }; return MangaInfo.chapters.push( - !langChapter.includes("?lang=id") ? MangaInfoChapter : null, + !langChapter.includes("?lang=id") ? MangaInfoChapter : null ); - }, + } ); return MangaInfo; @@ -159,7 +163,7 @@ export class Comick { } const { data } = await axios.get( - `${this.url}/comic/${title}/${urlchange}`, + `${this.url}/comic/${title}/${urlchange}` ); const $ = cheerio.load(data); @@ -181,7 +185,7 @@ export class Comick { name: e.name, image: "https://meo.comick.pictures/" + e.b2key, }; - }, + } ), cover: "https://meo.comick.pictures/" + @@ -200,7 +204,7 @@ export class Comick { ? `${title}/${hid}.json?slug=${title}&chapter=${hid}` : `${title}/${hid}-chapter-${idNumber}${currentLang}.json?slug=${title}&chapter=${hid}-chapter-${idNumber}${currentLang}`; const dataBuild = await axios.get( - `${this.url}/_next/data/${buildid}/comic/${currentUrl}`, + `${this.url}/_next/data/${buildid}/comic/${currentUrl}` ); const mindate = new Date(dataBuild.data.pageProps.chapter.created_at); @@ -218,7 +222,7 @@ export class Comick { name: s.name, image: "https://meo.comick.pictures/" + s.b2key, }; - }, + } ), cover: "https://meo.comick.pictures/" + diff --git a/src/scraper/sites/manga/inmanga/Inmanga.ts b/src/scraper/sites/manga/inmanga/Inmanga.ts index f78ba6de..3e7e7d36 100644 --- a/src/scraper/sites/manga/inmanga/Inmanga.ts +++ b/src/scraper/sites/manga/inmanga/Inmanga.ts @@ -1,7 +1,11 @@ import * as cheerio from "cheerio"; import axios from "axios"; -import { Manga, MangaChapter, IMangaResult } from "../../../../types/manga"; -import { IResultSearch } from "../../../../types/search"; +import { + Manga, + MangaChapter, + type IMangaResult, +} from "../../../../types/manga"; +import { type IResultSearch } from "../../../../types/search"; //Default Set Axios Cookie axios.defaults.withCredentials = true; @@ -83,7 +87,7 @@ export class Inmanga { if (genreList.includes(e)) { formdata.append( "filter[generes][]", - genreList[genreList.indexOf(e)], + genreList[genreList.indexOf(e)] ); } }); @@ -94,7 +98,7 @@ export class Inmanga { const bodyContent = formdata; const { data } = await axios.post( `${this.url}/manga/getMangasConsultResult`, - bodyContent, + bodyContent ); const $ = cheerio.load(data); @@ -141,7 +145,7 @@ export class Inmanga { altTitles: [], url: `/manga/inmanga/title/${manga}`, description: $_( - "body > div > section > div > div > div:nth-child(6) > div > div.panel-body", + "body > div > section > div > div > div:nth-child(6) > div > div.panel-body" ) .text() .trim(), @@ -160,19 +164,19 @@ export class Inmanga { }; $_( - ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .text-muted span", + ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .text-muted span" ).each((_i, e) => - MangaInfo.altTitles.push($_(e).text().replace(";", "")), + MangaInfo.altTitles.push($_(e).text().replace(";", "")) ); $_( - ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm", + ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm" ).each((_i, e) => MangaInfo.genres.push($_(e).text().trim())); MangaInfo.altTitles.slice(MangaInfo.altTitles.indexOf('""'), 0); MangaInfo.genres.slice(MangaInfo.genres.indexOf('""'), 0); const dataChPost = await axios.get( - `${this.url}/chapter/getall?mangaIdentification=${cid}`, + `${this.url}/chapter/getall?mangaIdentification=${cid}` ); const dataCh = JSON.parse(dataChPost.data.data); dataCh.result.map( @@ -196,7 +200,7 @@ export class Inmanga { }, }; MangaInfo.chapters.push(MangaInfoChapter); - }, + } ); return MangaInfo; @@ -211,7 +215,7 @@ export class Inmanga { const idNumber = Number(manga.substring(manga.lastIndexOf("-") + 1)); const { data } = await axios.get( - `${this.url}/chapter/chapterIndexControls?identification=${cid}`, + `${this.url}/chapter/chapterIndexControls?identification=${cid}` ); const $ = cheerio.load(data); diff --git a/src/scraper/sites/manga/manganelo/Manganelo.ts b/src/scraper/sites/manga/manganelo/Manganelo.ts index 8a59f1ff..5e8f0c01 100644 --- a/src/scraper/sites/manga/manganelo/Manganelo.ts +++ b/src/scraper/sites/manga/manganelo/Manganelo.ts @@ -3,7 +3,7 @@ import axios from "axios"; import { load } from "cheerio"; import { Image } from "../../../../types/image"; import { ManganatoManagerUtils } from "./ManganatoManagerUtils"; -import { IManganatoFilterParams } from "./ManganatoTypes"; +import { type IManganatoFilterParams } from "./ManganatoTypes"; import { ResultSearch } from "../../../../types/search"; export class Manganelo { @@ -25,7 +25,7 @@ export class Manganelo { private GetMangaStatus(data: cheerio.Root) { const selector = data( - "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(3) > td.table-value", + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(3) > td.table-value" ); if (selector.length == 0) return null; @@ -36,7 +36,7 @@ export class Manganelo { private GetMangaAuthors(data: cheerio.Root): string[] | null { const selector = data( - "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(2) > td.table-value", + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(2) > td.table-value" ); if (selector.length == 0 && selector.find("a.a-h").length == 0) return null; @@ -51,7 +51,7 @@ export class Manganelo { private GetMangaGenres(data: cheerio.Root): string[] | null { const selector = data( - "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(4) > td.table-value", + "div.panel-story-info > div.story-info-right > table > tbody > tr:nth-child(4) > td.table-value" ); if (selector.length == 0 && selector.find("a.a-h").length == 0) return null; @@ -67,7 +67,7 @@ export class Manganelo { private isNsfw(genres: string[]) { return genres.some( (genre) => - genre === "Pornographic" || genre === "Mature" || genre === "Erotica", + genre === "Pornographic" || genre === "Mature" || genre === "Erotica" ); } @@ -124,7 +124,7 @@ export class Manganelo { const thumbnail = this.url + $( - "div.panel-story-info > div.story-info-left > span.info-image > img", + "div.panel-story-info > div.story-info-left > span.info-image > img" ).attr("src"); const altTitle = $("table > tbody > tr:nth-child(1) > td.table-value > h2") .text() @@ -181,7 +181,7 @@ export class Manganelo { async GetMangaChapters(mangaId: string, chapterNumber: number) { const { data } = await axios.get( - `${this.url}/chapter/manga-${mangaId}/chapter-${chapterNumber}`, + `${this.url}/chapter/manga-${mangaId}/chapter-${chapterNumber}` ); const $ = load(data); diff --git a/src/scraper/sites/manga/manganelo/managers/ManganatoURLManager.ts b/src/scraper/sites/manga/manganelo/managers/ManganatoURLManager.ts index cf1e266a..abccecfa 100644 --- a/src/scraper/sites/manga/manganelo/managers/ManganatoURLManager.ts +++ b/src/scraper/sites/manga/manganelo/managers/ManganatoURLManager.ts @@ -1,6 +1,6 @@ import { URLSearchParams } from "url"; import { - IManganatoFilterParams, + type IManganatoFilterParams, ManganatoFilterURLParams, manganatoGenreList, manganatoOrderByOptions, @@ -43,7 +43,7 @@ export class ManganatoAdvancedSearchURLManager extends ManganatoManager { private processOrderBy(order: unknown) { return typeof order === "string" && manganatoOrderByOptionsList.includes( - order.toLowerCase() as manganatoOrderByOptions, + order.toLowerCase() as manganatoOrderByOptions ) ? order : ""; diff --git a/src/scraper/sites/manga/nhentai/Nhentai.ts b/src/scraper/sites/manga/nhentai/Nhentai.ts index 7593b9b0..61496c22 100644 --- a/src/scraper/sites/manga/nhentai/Nhentai.ts +++ b/src/scraper/sites/manga/nhentai/Nhentai.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { load } from "cheerio"; import { getFilterByPages } from "./assets/getFilterByPage"; -import { IMangaChapter, Manga } from "../../../../types/manga"; +import { type IMangaChapter, Manga } from "../../../../types/manga"; export class Nhentai { async filter(mangaName: string) { @@ -102,9 +102,9 @@ class NhentaiGetMangaChapters { mangaImagesPages.push( $(chapterImage) .attr("data-src") - .replace("cdn.dogehls.xyz", "t7.nhentai.net"), + .replace("cdn.dogehls.xyz", "t7.nhentai.net") ); - }, + } ); mangaChapters.push({ From b7d7587b3a5288d308b011588acb04616cab10b0 Mon Sep 17 00:00:00 2001 From: koikiss-dev Date: Thu, 21 Mar 2024 22:05:11 -0600 Subject: [PATCH 06/64] pm2 option and bun.lock --- bun.lockb | Bin 0 -> 409083 bytes package.json | 1 + 2 files changed, 1 insertion(+) create mode 100755 bun.lockb diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..3edfe856add605a9298572b48381cfa83f4f10b2 GIT binary patch literal 409083 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 {qU1lI#4LHno|7Fr(1yM82URNARq-Q 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 Date: Sun, 14 Apr 2024 12:21:35 -0400 Subject: [PATCH 07/64] Add Search Animevostfr and remove comments in AnimeLatinoHD --- bun.lockb | Bin 409083 -> 409083 bytes .../v1/anime/animevostfr/AnimevostfrRoutes.ts | 7 +- .../anime/animelatinohd/AnimeLatinoHD.ts | 22 ----- .../sites/anime/animevostfr/Animevostfr.ts | 93 ++++++++---------- 4 files changed, 42 insertions(+), 80 deletions(-) diff --git a/bun.lockb b/bun.lockb index 3edfe856add605a9298572b48381cfa83f4f10b2..2abee68d7164e70fb7eea4fa88066b208d7aba5a 100755 GIT binary patch delta 8892 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 GTmKKHrL#W( diff --git a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts index a786c5d9..f9e14a4a 100644 --- a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts +++ b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts @@ -5,14 +5,11 @@ const router = Router(); // Filter router.get("/anime/animevostfr/filter", async (req, res) => { - const { search, type, page, year, genre } = req.query; + const { search, page } = req.query; const data = await Anime.GetAnimeByFilter( search as string, - type as unknown as number, - page as unknown as number, - year as string, - genre as string, + page as unknown as number ); res.send(data); }); diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index cc5ad4cc..31dce164 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -102,8 +102,6 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( async (e: { server: { title: string }; id: string }) => { - //let min = await axios.get("https://api.animelatinohd.com/stream/" + e.id, { headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36 Edg/116.0.1938.62", "Referer": "https://www.animelatinohd.com/" } }) - // let dat = cheerio.load(min.data) const Server: EpisodeServer = { name: e.server.title, @@ -112,26 +110,6 @@ export class AnimeLatinoHD extends AnimeProviderModel { Server.url = "https://api.animelatinohd.com/stream/" + e.id; Server.name = e.server.title; - //state 1 - /*if (e.server.title == "Beta") { - let sel = dat("script:contains('var foo_ui = function (event) {')") - let sort = String(sel.html()) - let domain = eval(sort.slice(sort.search("const url"), sort.search("const langDef")).replace("const url =", "").trim()) - - let sortMORE = sort.slice(sort.search('ajax'), sort.search("method: 'post',")) - let obj_sort = sortMORE.replace("ajax({", "").trim().replace("url:", "").replace(",", "").replace('"', "").replace('"', "").trim() - let id_file = obj_sort.slice(obj_sort.lastIndexOf("/"), obj_sort.length) - Server.url = domain + "/v" + id_file - - } else if (e.server.title == "Gamma") { - Server.url = dat('meta[name="og:url"]').attr("content") - } else { - let sel = dat("script[data-cfasync='false']") - let sort = String(sel.html()) - let sortMORE = sort.slice(sort.lastIndexOf("master") + 7, sort.lastIndexOf("hls2") - 11) - let id_file = sortMORE.replace("_x", "") - Server.url = "https://filemoon.sx" + "/e/" + id_file - }*/ AnimeEpisodeInfo.servers.push(Server); } ) diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index c9334a01..c1436d93 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -9,7 +9,6 @@ import { type IAnimeSearch, } from "../../../../types/search"; import { AnimeProviderModel } from "../../../ScraperAnimeModel"; -//import { Calendar } from "@animetypes/date"; /** List of Domains * @@ -19,7 +18,6 @@ import { AnimeProviderModel } from "../../../ScraperAnimeModel"; export class Animevostfr extends AnimeProviderModel { readonly url = "https://animevostfr.tv"; - readonly api = "https://api.animelatinohd.com"; async GetAnimeInfo(anime: string): Promise { try { @@ -49,7 +47,7 @@ export class Animevostfr extends AnimeProviderModel { alt_name: [ ...AnimeDescription.slice( AnimeDescription.indexOf("Titre alternatif:") + - "Titre alternatif:".length, + "Titre alternatif:".length, AnimeDescription.indexOf("Synopsis:") ) .replace("
\n", "") @@ -71,8 +69,8 @@ export class Animevostfr extends AnimeProviderModel { AnimeTypes == "Anime" ? "Anime" : AnimeTypes == "MOVIE" - ? "Movie" - : "Null", //tv,pelicula,especial,ova + ? "Movie" + : "Null", //tv,pelicula,especial,ova status: AnimeStatus == "En cours" ? true : false, date: AnimeDate ? { year: AnimeDate } : null, episodes: [], @@ -109,13 +107,10 @@ export class Animevostfr extends AnimeProviderModel { ); const $ = cheerio.load(data); const s = $(".form-group.list-server select option"); - const e = $(".list-episodes select option"); - const ListFilmId = []; + const e = $(`.list-episodes select option[value='${number}']`); + const ListFilmId = $(e).attr("episodeid"); const ListServer = []; - s.map((_i, e) => ListServer.push($(e).val())); - e.map((_i, e) => ListFilmId.push($(e).attr("episodeid"))); - /* "SERVER_VIP" "SERVER_HYDRAX" @@ -138,76 +133,68 @@ export class Animevostfr extends AnimeProviderModel { image: "", servers: [], }; - await Promise.all( - ListServer.map(async (n) => { - const servers = await axios.get( - `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId[0]}` - ); - let currentData = servers.data; - if (n == "opencdn" || n == "photo") { + ListServer.map(async (n: string) => { + + if (n == "opencdn" || n == "photo" || n == "vip") { + + const sservers = await axios.get( + `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId}` + ); + let currentData = sservers.data; currentData = currentData - .replace("?logo=https://animevostfr.tv/1234.png", "") + .replace(`?logo=${this.url}/1234.png`, "") + .replace("hydrax.net/watch", "abysscdn.com/") .replace("short.ink/", "abysscdn.com/?v="); + console.log(n, currentData) + let Servers: EpisodeServer = { + name: n, + url: currentData, + }; + + AnimeEpisodeInfo.servers.push(Servers); } - const Servers: EpisodeServer = { - name: n, - url: currentData, - }; - AnimeEpisodeInfo.servers.push(Servers); - }) - ); - AnimeEpisodeInfo.servers.sort( - (a: EpisodeServer, b: EpisodeServer) => a.name.length - b.name.length - ); + return AnimeEpisodeInfo + }) + ) return AnimeEpisodeInfo; } catch (error) { - console.log(error); } } async GetAnimeByFilter( search?: string, - type?: number, - page?: number, - year?: string, - genre?: string + page?: number ): Promise> { try { - const { data } = await axios.get(`${this.api}/api/anime/list`, { + const { data } = await axios.get(`${this.url}/page/${page ? page : 1}`, { params: { - search: search, - type: type, - year: year, - genre: genre, - page: page, + s: search }, }); - const animeSearchParseObj = data; + const $ = cheerio.load(data); const animeSearch: ResultSearch = { nav: { - count: animeSearchParseObj.data.length, - current: animeSearchParseObj.current_page, + count: $(".movies-list .ml-item").length, + current: page ? Number(page) : 1, next: - animeSearchParseObj.data.length < 28 + $(".movies-list .ml-item").length < 32 ? 0 - : animeSearchParseObj.current_page + 1, - hasNext: animeSearchParseObj.data.length < 28 ? false : true, + : Number(page) + 1, + hasNext: $(".movies-list .ml-item").length < 32 ? false : true, }, results: [], }; - animeSearchParseObj.data.map((e) => { + + $(".movies-list .ml-item").each((_i, e) => { const animeSearchData: AnimeSearch = { - name: e.name, - image: - "https://www.themoviedb.org/t/p/original" + - e.poster + - "?&w=53&q=95", - url: `/anime/animelatinohd/name/${e.slug}`, - type: "", + name: $(e).find(".mli-info").text(), + image: $(e).find(".mli-thumb").attr("data-original"), + url: `/anime/animevostfr/name/${$(e).find(".ml-mask").attr("href").replace(this.url, "").replace("/", "").replace("/", "")}`, + type: $(e).find(".mli-quality").text().includes("Movie") ? "movie" : "anime", }; animeSearch.results.push(animeSearchData); }); From bc00df3e28b2f28fa4c7807fcc2b8addfa853c01 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Mon, 29 Apr 2024 10:50:27 -0500 Subject: [PATCH 08/64] feat: add scraper models --- src/models/AnimeScraperModel.ts | 8 ++++++++ src/models/BaseScraperModel.ts | 8 ++++++++ src/models/MangaScraperModel.ts | 6 ++++++ src/scraper/ScraperAnimeModel.ts | 15 --------------- 4 files changed, 22 insertions(+), 15 deletions(-) create mode 100644 src/models/AnimeScraperModel.ts create mode 100644 src/models/BaseScraperModel.ts create mode 100644 src/models/MangaScraperModel.ts delete mode 100644 src/scraper/ScraperAnimeModel.ts diff --git a/src/models/AnimeScraperModel.ts b/src/models/AnimeScraperModel.ts new file mode 100644 index 00000000..4b3a1e0c --- /dev/null +++ b/src/models/AnimeScraperModel.ts @@ -0,0 +1,8 @@ +import type { Anime } from "../types/anime"; +import type { IAnimeSearch } from "../types/search"; +import type { Episode } from "../types/episode"; +import { BaseScraperModel } from "./BaseScraperModel"; + +export abstract class AnimeScraperModel extends BaseScraperModel { + public abstract GetEpisodeServers(...args: unknown[]): Promise; +} diff --git a/src/models/BaseScraperModel.ts b/src/models/BaseScraperModel.ts new file mode 100644 index 00000000..ece424bd --- /dev/null +++ b/src/models/BaseScraperModel.ts @@ -0,0 +1,8 @@ +import type { IResultSearch } from "@animetypes/search"; + +export abstract class BaseScraperModel { + public abstract readonly url: string; + + public abstract GetItemInfo(item: string): Promise; + public abstract GetItemByFilter(...args: unknown[]): Promise>; +} diff --git a/src/models/MangaScraperModel.ts b/src/models/MangaScraperModel.ts new file mode 100644 index 00000000..548b2499 --- /dev/null +++ b/src/models/MangaScraperModel.ts @@ -0,0 +1,6 @@ +import type { IMangaResult, Manga, MangaChapter, MangaVolume } from "@animetypes/manga"; +import { BaseScraperModel } from "./BaseScraperModel"; + +export abstract class MangaScraperModel extends BaseScraperModel { + public abstract GetMangaChapters(...args: unknown[]): Promise +} diff --git a/src/scraper/ScraperAnimeModel.ts b/src/scraper/ScraperAnimeModel.ts deleted file mode 100644 index 0b9dda79..00000000 --- a/src/scraper/ScraperAnimeModel.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Anime } from "../types/anime"; -import { type IResultSearch, type IAnimeSearch } from "../types/search"; -import { Episode } from "../types/episode"; - -export abstract class AnimeProviderModel { - abstract readonly url: string; - - abstract GetAnimeInfo(anime: string): Promise; - - abstract GetAnimeByFilter( - ...args: any[] - ): Promise>; - - abstract GetEpisodeServers(...args: any[]): Promise; -} From 9b257a913749021933e78ce76274cd969c748a12 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Mon, 29 Apr 2024 20:16:45 -0500 Subject: [PATCH 09/64] fix: import class --- src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts | 4 ++-- src/routes/v1/anime/animeflv/AnimeflvRoutes.ts | 4 ++-- .../anime/animelatinohd/AnimeLatinoHDRoutes.ts | 4 ++-- .../v1/anime/animevostfr/AnimevostfrRoutes.ts | 4 ++-- src/routes/v1/anime/wcostream/wcostreamRoutes.ts | 4 ++-- src/routes/v1/anime/zoro/ZoroRoutes.ts | 4 ++-- src/scraper/sites/anime/animeBlixs/AnimeBlix.ts | 8 ++++---- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 8 ++++---- .../sites/anime/animelatinohd/AnimeLatinoHD.ts | 8 ++++---- .../sites/anime/animevostfr/Animevostfr.ts | 16 ++++++++-------- src/scraper/sites/anime/wcostream/WcoStream.ts | 8 ++++---- src/scraper/sites/anime/zoro/Zoro.ts | 8 ++++---- src/test/Animeflv.spec.ts | 4 ++-- src/test/Animelatinohd.spec.ts | 4 ++-- src/test/Zoro.spec.ts | 4 ++-- 15 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts b/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts index 3e29104d..e7172a56 100644 --- a/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts +++ b/src/routes/v1/anime/animeblix/AnimeBlixRoutes.ts @@ -8,7 +8,7 @@ const router = Router(); router.get("/anime/animeblix/filter", async (req, res) => { const { search, type, page, year, genre } = req.query; - const data = await Anime.GetAnimeByFilter( + const data = await Anime.GetItemByFilter( search as string, type as unknown as number, page as unknown as number, @@ -21,7 +21,7 @@ router.get("/anime/animeblix/filter", async (req, res) => { // Anime Info +(Episodes list) router.get("/anime/animeblix/name/:name", async (req, res) => { const { name } = req.params; - const data = await Anime.GetAnimeInfo( + const data = await Anime.GetItemInfo( name.includes("ver-") ? name.replace("ver-", "") : name, ); res.send(data); diff --git a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts index d230be2d..3c671f9b 100644 --- a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts +++ b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts @@ -13,7 +13,7 @@ r.get("/anime/flv/name/:name", async (req, res) => { try { const { name } = req.params; const flv = new AnimeFlv(); - const animeInfo = await flv.GetAnimeInfo(name); + const animeInfo = await flv.GetItemInfo(name); res.send(animeInfo); } catch (error) { console.log(error); @@ -46,7 +46,7 @@ r.get("/anime/flv/filter", async (req, res) => { const title = req.query.title as string; const flv = new AnimeFlv(); - const animeInfo = await flv.GetAnimeByFilter( + const animeInfo = await flv.GetItemByFilter( gen, date, type, diff --git a/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts b/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts index d9a380cc..35836c61 100644 --- a/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts +++ b/src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes.ts @@ -7,7 +7,7 @@ const router = Router(); router.get("/anime/animelatinohd/filter", async (req, res) => { const { search, type, page, year, genre } = req.query; - const data = await Anime.GetAnimeByFilter( + const data = await Anime.GetItemByFilter( search as string, type as unknown as number, page as unknown as number, @@ -20,7 +20,7 @@ router.get("/anime/animelatinohd/filter", async (req, res) => { // Anime Info +(Episodes list) router.get("/anime/animelatinohd/name/:name", async (req, res) => { const { name } = req.params; - const data = await Anime.GetAnimeInfo(name); + const data = await Anime.GetItemInfo(name); res.send(data); }); diff --git a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts index a786c5d9..1f2c5149 100644 --- a/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts +++ b/src/routes/v1/anime/animevostfr/AnimevostfrRoutes.ts @@ -7,7 +7,7 @@ const router = Router(); router.get("/anime/animevostfr/filter", async (req, res) => { const { search, type, page, year, genre } = req.query; - const data = await Anime.GetAnimeByFilter( + const data = await Anime.GetItemByFilter( search as string, type as unknown as number, page as unknown as number, @@ -20,7 +20,7 @@ router.get("/anime/animevostfr/filter", async (req, res) => { // Anime Info +(Episodes list) router.get("/anime/animevostfr/name/:name", async (req, res) => { const { name } = req.params; - const data = await Anime.GetAnimeInfo(name); + const data = await Anime.GetItemInfo(name); res.send(data); }); diff --git a/src/routes/v1/anime/wcostream/wcostreamRoutes.ts b/src/routes/v1/anime/wcostream/wcostreamRoutes.ts index f6d0a56d..0e5637eb 100644 --- a/src/routes/v1/anime/wcostream/wcostreamRoutes.ts +++ b/src/routes/v1/anime/wcostream/wcostreamRoutes.ts @@ -6,7 +6,7 @@ const router = Router(); router.get("/anime/wcostream/name/:name", async (req, res) => { const { name } = req.params; - const data = await Anime.GetAnimeInfo(name); + const data = await Anime.GetItemInfo(name); res.send(data); }); @@ -24,7 +24,7 @@ router.get("/anime/wcostream/episode/:episode", async (req, res) => { router.get("/anime/wcostream/filter", async (req, res) => { const { search, page } = req.query; - const data = await Anime.GetAnimeByFilter( + const data = await Anime.GetItemByFilter( search as string, page as unknown as number, ); diff --git a/src/routes/v1/anime/zoro/ZoroRoutes.ts b/src/routes/v1/anime/zoro/ZoroRoutes.ts index d163b409..cdcc825b 100644 --- a/src/routes/v1/anime/zoro/ZoroRoutes.ts +++ b/src/routes/v1/anime/zoro/ZoroRoutes.ts @@ -7,7 +7,7 @@ r.get("/anime/zoro/name/:name", async (req, res) => { try { const { name } = req.params; const zoro = new Zoro(); - const animeInfo = await zoro.GetAnimeInfo(name); + const animeInfo = await zoro.GetItemInfo(name); res.send(animeInfo); } catch (error) { console.log(error); @@ -41,7 +41,7 @@ r.get("/anime/zoro/filter", async (req, res) => { const page = req.query.page as string; const zoro = new Zoro(); - const animeInfo = await zoro.GetAnimeByFilter( + const animeInfo = await zoro.GetItemByFilter( type, rated, score, diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index 83aa1a55..f78dd095 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -8,7 +8,7 @@ import { type IResultSearch, type IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains @@ -20,11 +20,11 @@ import { AnimeProviderModel } from "../../../ScraperAnimeModel"; * */ -export class AnimeBlix extends AnimeProviderModel { +export class AnimeBlix extends AnimeScraperModel { readonly url = "https://vwv.animeblix.org"; readonly api = "https://api.animelatinohd.com"; - async GetAnimeInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get( `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}` @@ -165,7 +165,7 @@ export class AnimeBlix extends AnimeProviderModel { } } - async GetAnimeByFilter( + async GetItemByFilter( search?: string, type?: number, page?: number, diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 5c43387c..ab0330b2 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -14,12 +14,12 @@ import { type IResultSearch, type IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; -export class AnimeFlv extends AnimeProviderModel { +export class AnimeFlv extends AnimeScraperModel { readonly url = "https://animeflv.ws"; - async GetAnimeInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = load(data); @@ -76,7 +76,7 @@ export class AnimeFlv extends AnimeProviderModel { } } - async GetAnimeByFilter( + async GetItemByFilter( gen?: Genres | string, date?: string, type?: TypeAnimeflv, diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index cc5ad4cc..b22394f6 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -8,13 +8,13 @@ import { type IResultSearch, type IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; -export class AnimeLatinoHD extends AnimeProviderModel { +export class AnimeLatinoHD extends AnimeScraperModel { readonly url = "https://www.animelatinohd.com"; readonly api = "https://api.animelatinohd.com"; - async GetAnimeInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = cheerio.load(data); @@ -143,7 +143,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { } } - async GetAnimeByFilter( + async GetItemByFilter( search?: string, type?: number, page?: number, diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index c9334a01..c80c9a46 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -8,7 +8,7 @@ import { type IResultSearch, type IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; //import { Calendar } from "@animetypes/date"; /** List of Domains @@ -17,11 +17,11 @@ import { AnimeProviderModel } from "../../../ScraperAnimeModel"; * */ -export class Animevostfr extends AnimeProviderModel { +export class Animevostfr extends AnimeScraperModel { readonly url = "https://animevostfr.tv"; readonly api = "https://api.animelatinohd.com"; - async GetAnimeInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/${anime}`); const $ = cheerio.load(data); @@ -120,12 +120,12 @@ export class Animevostfr extends AnimeProviderModel { "SERVER_VIP" "SERVER_HYDRAX" "SERVER_PHOTOSS" - "SERVER_DOWNLOAD" - "SERVER_PHOTOS" + "SERVER_DOWNLOAD" + "SERVER_PHOTOS" "SERVER_OPEN_LOAD" - "SERVER_OPEN_LOADS" + "SERVER_OPEN_LOADS" "SERVER_OPEN_CDN" - "SERVER_OPEN_CDNO" + "SERVER_OPEN_CDNO" "SERVER_PHOTO" "SERVER_STREAM_MANGO" "SERVER_RAPID_VIDEO" @@ -167,7 +167,7 @@ export class Animevostfr extends AnimeProviderModel { } } - async GetAnimeByFilter( + async GetItemByFilter( search?: string, type?: number, page?: number, diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index 730b6cb4..fabffe21 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -9,7 +9,7 @@ import { AnimeSearch, } from "../../../../types/search"; import { UnPacked } from "../../../../types/utils"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; /** List of Domains * https://wcostream.tv @@ -36,10 +36,10 @@ axios.defaults.withCredentials = true; axios.defaults.headers.common["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36 Edg/117.0.2045.55"; -export class WcoStream extends AnimeProviderModel { +export class WcoStream extends AnimeScraperModel { readonly url = "https://www.wcostream.tv"; - async GetAnimeInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`, { headers: { @@ -222,7 +222,7 @@ export class WcoStream extends AnimeProviderModel { } } - async GetAnimeByFilter( + async GetItemByFilter( search?: string, page?: number ): Promise> { diff --git a/src/scraper/sites/anime/zoro/Zoro.ts b/src/scraper/sites/anime/zoro/Zoro.ts index 67acb943..36aa3c8c 100644 --- a/src/scraper/sites/anime/zoro/Zoro.ts +++ b/src/scraper/sites/anime/zoro/Zoro.ts @@ -1,5 +1,6 @@ import axios from "axios"; import { load } from "cheerio"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; import { Anime, Chronology } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { @@ -7,12 +8,11 @@ import { ResultSearch, type IAnimeSearch, } from "../../../../types/search"; -import { AnimeProviderModel } from "../../../ScraperAnimeModel"; -export class Zoro extends AnimeProviderModel { +export class Zoro extends AnimeScraperModel { readonly url = "https://aniwatch.to"; - async GetAnimeInfo(animeName: string): Promise { + async GetItemInfo(animeName: string): Promise { try { const response = await axios.get(`${this.url}/${animeName}`); const $ = load(response.data); @@ -63,7 +63,7 @@ export class Zoro extends AnimeProviderModel { } } //filter - async GetAnimeByFilter( + async GetItemByFilter( type?: string, rated?: string, score?: string, diff --git a/src/test/Animeflv.spec.ts b/src/test/Animeflv.spec.ts index 42196614..adcee4f7 100644 --- a/src/test/Animeflv.spec.ts +++ b/src/test/Animeflv.spec.ts @@ -11,7 +11,7 @@ describe("AnimeFlv", () => { }); it("should get anime info successfully", async () => { - const animeInfo = await animeFlv.GetAnimeInfo("wonder-egg-priority"); + const animeInfo = await animeFlv.GetItemInfo("wonder-egg-priority"); expect(animeInfo.name).toBe("Wonder Egg Priority"); expect(animeInfo.alt_name).toContain("ワンダーエッグ・プライオリティ"); expect(animeInfo.image.url).toContain(".jpg"); @@ -23,7 +23,7 @@ describe("AnimeFlv", () => { }); it("should filter anime successfully", async () => { - const result = await animeFlv.GetAnimeByFilter( + const result = await animeFlv.GetItemByFilter( Genres.Action, "all", "all", diff --git a/src/test/Animelatinohd.spec.ts b/src/test/Animelatinohd.spec.ts index 2fb063c9..455b6de2 100644 --- a/src/test/Animelatinohd.spec.ts +++ b/src/test/Animelatinohd.spec.ts @@ -8,7 +8,7 @@ describe("AnimeLatinohd", () => { }); it("should get anime info successfully", async () => { - const animeInfo = await animelatinohd.GetAnimeInfo("wonder-egg-priority"); + const animeInfo = await animelatinohd.GetItemInfo("wonder-egg-priority"); expect(animeInfo.name).toBe("Wonder Egg Priority"); expect(animeInfo.alt_name).toContain("ワンダーエッグ・プライオリティ"); expect(animeInfo.image.url).toContain(".jpg"); @@ -19,7 +19,7 @@ describe("AnimeLatinohd", () => { }); it("should filter anime successfully", async () => { - const result = await animelatinohd.GetAnimeByFilter(); + const result = await animelatinohd.GetItemByFilter(); expect(result.results.length).toBeGreaterThan(0); }, 10000); }); diff --git a/src/test/Zoro.spec.ts b/src/test/Zoro.spec.ts index e2ad3665..5e4d1654 100644 --- a/src/test/Zoro.spec.ts +++ b/src/test/Zoro.spec.ts @@ -7,7 +7,7 @@ describe("Zoro", () => { zoro = new Zoro(); }); it("should get anime info successfully", async () => { - const animeInfo = await zoro.GetAnimeInfo("tokyo-ghoul-790"); + const animeInfo = await zoro.GetItemInfo("tokyo-ghoul-790"); expect(animeInfo.name).toBe("Tokyo Ghoul"); expect(animeInfo.alt_name).toContain("東京喰種-トーキョーグール-"); expect(animeInfo.image.url).toContain(".jpg"); @@ -16,7 +16,7 @@ describe("Zoro", () => { expect(animeInfo.genres.length).toBeGreaterThan(0); }); it("should filter anime successfully", async () => { - const result = await zoro.GetAnimeByFilter("2"); + const result = await zoro.GetItemByFilter("2"); expect(result.results.length).toBeGreaterThan(0); }); }); From ef2202ef15f4c758b5e3304a516991d661f7e6a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Wed, 1 May 2024 20:41:36 -0400 Subject: [PATCH 10/64] fix: Comick (request data) --- src/routes/v1/manga/comick/ComickRoutes.ts | 6 +- .../sites/anime/animevostfr/Animevostfr.ts | 5 - src/scraper/sites/manga/comick/Comick.ts | 105 ++++++++++-------- 3 files changed, 63 insertions(+), 53 deletions(-) diff --git a/src/routes/v1/manga/comick/ComickRoutes.ts b/src/routes/v1/manga/comick/ComickRoutes.ts index 69e2c1fb..f6bf3895 100644 --- a/src/routes/v1/manga/comick/ComickRoutes.ts +++ b/src/routes/v1/manga/comick/ComickRoutes.ts @@ -4,13 +4,15 @@ const Manga = new Comick(); const router = Router(); router.get("/manga/comick/filter", async (req, res) => { - const { search, type, year, genre } = req.query; + const { search, type, year, genre, page,status } = req.query; const data = await Manga.GetMangaByFilter( search as string, type as unknown as number, - year as string, + year as unknown as number, + status as unknown as number, genre as string, + page as unknown as number ); res.send(data); diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index c1436d93..d15b287f 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -135,9 +135,7 @@ export class Animevostfr extends AnimeProviderModel { }; await Promise.all( ListServer.map(async (n: string) => { - if (n == "opencdn" || n == "photo" || n == "vip") { - const sservers = await axios.get( `${this.url}/ajax-get-link-stream/?server=${n}&filmId=${ListFilmId}` ); @@ -146,15 +144,12 @@ export class Animevostfr extends AnimeProviderModel { .replace(`?logo=${this.url}/1234.png`, "") .replace("hydrax.net/watch", "abysscdn.com/") .replace("short.ink/", "abysscdn.com/?v="); - console.log(n, currentData) let Servers: EpisodeServer = { name: n, url: currentData, }; - AnimeEpisodeInfo.servers.push(Servers); } - return AnimeEpisodeInfo }) ) diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index c898807c..186dc7a9 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -30,21 +30,31 @@ export class Comick { async GetMangaByFilter( search?: string, + status?: number, type?: number, - year?: string, - genre?: string + year?: number, + genre?: string, + page?: number ) { try { const { data } = await axios.get(`${this.api}/v1.0/search`, { params: { q: search, - status: type, + status: status, + type:type, year: year, + page: page, genre: genre, }, }); - const ResultList: IResultSearch = { + nav: { count: data.length, + current: page ? page : 1, + next: + data.length < 49 + ? 0 + : page + 1, + hasNext: data.length < 49 ? false : true, }, results: [], }; data.map( @@ -74,16 +84,20 @@ export class Comick { async GetMangaInfo(manga: string, lang: string): Promise { try { - const { data } = await axios.get(`${this.api}/comic/${manga}`); - // build static - ///_next/data/S1XqseNRmzozm3TaUH1lU/comic/00-solo-leveling.json - const currentLang = lang ? `?lang=${lang}` : `?lang=en`; - const mangaInfoParseObj = data; - - const dataApi = await axios.get( - `${this.api}/comic/${mangaInfoParseObj.comic.hid}/chapters${currentLang}` + const { data } = await axios.get( + `${this.url}/comic/${manga}` ); - + const $ = cheerio.load(data); + const mangaInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()) + .props.pageProps; + const buildId = JSON.parse($("#__NEXT_DATA__").html()).buildId; + const currentLang = lang ? `?lang=${lang}` : `?lang=en`; + let dataApi = null + if (mangaInfoParseObj.firstChap) { + dataApi = await axios.get( + `${this.url}/_next/data/${buildId}/comic/${manga}/${mangaInfoParseObj.firstChap.hid + "-chapter-" + mangaInfoParseObj.firstChap.chap + "-" + mangaInfoParseObj.firstChap.lang}.json` + ); + } const MangaInfo: Manga = { id: mangaInfoParseObj.comic.id, title: mangaInfoParseObj.comic.title, @@ -106,40 +120,39 @@ export class Comick { mangaInfoParseObj.comic.md_covers[0].b2key, }, }; + if (mangaInfoParseObj.firstChap) { + dataApi.data.pageProps.chapters.map( + (e: { + id: number; + title: string; + hid: string; + chap: number; + created_at: string; + lang: string; + }) => { + const mindate = new Date(e.created_at); + const langChapter = currentLang ? currentLang : "?lang=" + e.lang; - dataApi.data.chapters.map( - (e: { - id: number; - title: string; - hid: string; - chap: number; - created_at: string; - lang: string; - }) => { - const mindate = new Date(e.created_at); - const langChapter = currentLang ? currentLang : "?lang=" + e.lang; - - const MangaInfoChapter: MangaChapter = { - id: e.id, - title: e.title, - url: `/manga/comick/chapter/${e.hid}-${ - mangaInfoParseObj.comic.slug - }-${e.chap ? e.chap : "err"}${langChapter}`, - number: e.chap, - images: null, - cover: null, - date: { - year: mindate.getFullYear() ? mindate.getFullYear() : null, - month: mindate.getMonth() ? mindate.getMonth() : null, - day: mindate.getDay() ? mindate.getDay() : null, - }, - }; - return MangaInfo.chapters.push( - !langChapter.includes("?lang=id") ? MangaInfoChapter : null - ); - } - ); - + const MangaInfoChapter: MangaChapter = { + id: e.id, + title: e.title, + url: `/manga/comick/chapter/${e.hid}-${mangaInfoParseObj.comic.slug + }-${e.chap ? e.chap : "err"}${langChapter}`, + number: e.chap, + images: null, + cover: null, + date: { + year: mindate.getFullYear() ? mindate.getFullYear() : null, + month: mindate.getMonth() ? mindate.getMonth() : null, + day: mindate.getDay() ? mindate.getDay() : null, + }, + }; + return MangaInfo.chapters.push( + !langChapter.includes("?lang=id") ? MangaInfoChapter : null + ); + } + ); + } return MangaInfo; } catch (error) { console.log(error); From 7361f6cdb71f2ea8a371569fc9a8a61dc75807b0 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Tue, 21 May 2024 20:11:44 -0500 Subject: [PATCH 11/64] feat(types)!: base abstractions BREAKING CHANGE: This will be the base class of all type of providers. --- src/types/base.ts | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/types/base.ts diff --git a/src/types/base.ts b/src/types/base.ts new file mode 100644 index 00000000..484aa2e7 --- /dev/null +++ b/src/types/base.ts @@ -0,0 +1,78 @@ +/** + * Here will stay all possible names of the types of providers. These options + * will be used to create a new URL of the media in the API. Modify it every + * time you need to add a new type of provider. + * + * Examples: `/anime/flv/...` or `/manga/comick/...` + * + * @author Victor + */ +export type MediaURLOptions = "anime" | "manga"; + +/** + * The base interface to create new types of providers. + * + * @author Victor + */ +export interface IBaseMedia { + /** Name or title of the media */ + name: string; + /** URL or location of the media in the API. */ + url: `/${MediaURLOptions}/${string}/name/${string}` | string; + /** Alternative names or titles in different languages. */ + alt_names?: string[] | string; + /** Description or synopsis of the media. */ + synopsis?: string; + /** Genres that apply to the media. */ + genres?: string[]; + /** Indicates the status of the media, such as ongoing, completed, on air, finished, or on hold. */ + status?: string | boolean | "ongoing" | "completed"; + /** Indicates if the content is intended for adult audiences. */ + nsfw?: boolean; +} + +/** + * The base interface to create new search results of any new type of provider. + * + * @author Victor + */ +export interface IBaseResult { + /** Name of the media that was the result of your search */ + name: string; + /** The media URL from the API */ + url: `/${MediaURLOptions}/${string}/name/${string}` | string; +} + +/** + * Create the structure of the base media. + * + * @author Victor + */ +export abstract class BaseMedia implements IBaseMedia { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + url: `/${MediaURLOptions}/${string}/name/${string}` | string; + /** @inheritdoc */ + alt_names?: string[] | string; + /** @inheritdoc */ + synopsis?: string; + /** @inheritdoc */ + genres?: string[]; + /** @inheritdoc */ + status?: string | boolean | "ongoing" | "completed"; + /** @inheritdoc */ + nsfw?: boolean; +} + +/** + * Create the result search of the base media. + * + * @author Victor + */ +export abstract class BaseResult implements IBaseResult { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + url: `/${MediaURLOptions}/${string}/name/${string}` | string; +} From a7d277a9d5717534897c17d7afb358be4e56ed1a Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Tue, 21 May 2024 20:15:03 -0500 Subject: [PATCH 12/64] feat(types)!: add anime types inherited from base types BREAKING CHANGE: Some properties of anime class has changed its name. --- src/types/anime.ts | 45 ++++++++------------------------------------- src/types/search.ts | 18 ++++++------------ 2 files changed, 14 insertions(+), 49 deletions(-) diff --git a/src/types/anime.ts b/src/types/anime.ts index 5d6b4c20..3e701391 100644 --- a/src/types/anime.ts +++ b/src/types/anime.ts @@ -1,5 +1,6 @@ //anime data return standard +import { BaseMedia, type IBaseMedia } from "./base"; import { ICalendar, IDatePeriod } from "./date"; import { IEpisode } from "./episode"; import { IImage } from "./image"; @@ -37,29 +38,19 @@ export interface IChronology { * Spectify the anime structure that you scrapped * @author Zukaritasu */ -export interface IAnime { - /** Name of the anime */ - name: string; - /** Alternative names describing the name of the anime in another language */ - alt_name?: string | string[]; - /** Anime identifier that can be used when the anime name is not used in the URL. */ - id?: number; - /** The URL or location of the anime in the API */ - url: `/anime/${string}/name/${string}` | string; - /** The anime synopsis */ - synopsis?: string; +export interface IAnimeMedia extends IBaseMedia { /** * An IImage interface object representing the anime * image and its banner. */ image: IImage; + /** Anime identifier that can be used when the anime name is not used in the URL. */ + id?: number; /** * The date from when the anime started until it ended. The end date may be * auxiliary in case the anime has not ended. */ date?: IDatePeriod | ICalendar; /** The type of anime that indicates whether it is a movie, a special, TV, etc.. */ type?: AnimeType; - /** Genres that apply to anime */ - genres?: string[]; /** Climatic station of which the anime was released */ station?: ClimaticStation | string; /** @@ -72,12 +63,6 @@ export interface IAnime { * A list of the episodes of this anime. This property must be null or not used * if an IAnime object is used in IChronology. */ episodes?: IEpisode[]; - /** - * The status of the anime indicating whether it is on air, finished - * or still on hold. */ - status?: string | boolean; - /** Indicates whether the anime is adult content. */ - nsfw?: boolean; } /**---------------- Interfaces implementation ---------------- **/ @@ -120,35 +105,21 @@ export class Chronology implements IChronology { * Spectify the anime structure that you scrapped * @author Zukaritasu */ -export class Anime implements IAnime { - /** @inheritdoc */ - name: string; +export class AnimeMedia extends BaseMedia implements IAnimeMedia { /** @inheritdoc */ - alt_name?: string | string[]; + image: IImage; /** @inheritdoc */ id?: number; /** @inheritdoc */ - url: `/anime/${string}/name/${string}` | string; - /** @inheritdoc */ - synopsis: string; - /** @inheritdoc */ - image: IImage; - /** @inheritdoc */ date?: IDatePeriod | ICalendar; /** @inheritdoc */ type?: AnimeType; /** @inheritdoc */ - genres: string[] = []; + station?: ClimaticStation | string; /** @inheritdoc */ stats?: IAnimeStats; /** @inheritdoc */ - station?: ClimaticStation | string; - /** @inheritdoc */ chronology?: IChronology[]; /** @inheritdoc */ - episodes: IEpisode[] = []; - /** @inheritdoc */ - status?: string | boolean; - /** @inheritdoc */ - nsfw?: boolean; + episodes?: IEpisode[]; } diff --git a/src/types/search.ts b/src/types/search.ts index 035e465c..539a2981 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -1,5 +1,7 @@ //Spanish Providers - TypeScript version +import { BaseResult, type IBaseResult } from "./base"; + /** * Anime search helpers, use them with you scrapping by filter (searching..), * this format help you how you can return @@ -8,13 +10,9 @@ * @author Mawfyy * @author Zukaritasu */ -export interface IAnimeSearch { - /** Name of the anime that was the result of your search */ - name: string; +export interface IAnimeResult extends IBaseResult { /** The URL of the anime image */ image: string; - /** The anime URL from the API */ - url: `/anime/${string}/name/${string}` | string; // API url /** * Defines the type of content to which the anime is directed, which * can be a movie, OVA, ONA, etc... */ @@ -45,7 +43,7 @@ export interface IPageNavigation { * * @author Zukaritasu */ -export interface IResultSearch { +export interface IResultSearch { /** Search by navigation */ nav?: IPageNavigation; /** A list of the results obtained */ @@ -60,14 +58,10 @@ export interface IResultSearch { * @author Mawfyy * @author Zukaritasu */ -export class AnimeSearch implements IAnimeSearch { - /** @inheritdoc */ - name: string; +export class AnimeResult extends BaseResult implements IAnimeResult { /** @inheritdoc */ image: string; /** @inheritdoc */ - url: `/anime/${string}/name/${string}` | string; // API url - /** @inheritdoc */ type?: string; } @@ -77,7 +71,7 @@ export class AnimeSearch implements IAnimeSearch { * * @author Zukaritasu */ -export class ResultSearch implements IResultSearch { +export class ResultSearch implements IResultSearch { /** @inheritdoc */ nav?: IPageNavigation; /** @inheritdoc */ From d3712a751d8c58c7e9707c0ecd25b00d1dbc2370 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Tue, 21 May 2024 20:15:44 -0500 Subject: [PATCH 13/64] feat(types)!: add manga types inherited from base types BREAKING CHANGE: Some properties of manga class has changed its name. --- src/types/manga.ts | 51 ++++++++++------------------------------------ 1 file changed, 11 insertions(+), 40 deletions(-) diff --git a/src/types/manga.ts b/src/types/manga.ts index 529a5316..cba719e8 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -1,6 +1,7 @@ import { IImage } from "./image"; import { ICalendar } from "./date"; import { IResultSearch } from "./search"; +import { BaseMedia, type IBaseMedia, type IBaseResult } from "./base"; /** * The chapter is part of the manga and is also part of a volume. It is made @@ -79,27 +80,15 @@ export interface IMangaVolume { * * @author Zukaritasu */ -export interface IManga { +export interface IMangaMedia extends IBaseMedia { /** Manga ID */ id: number | string; - /** The URL of the manga in the API location */ - url: `/manga/${string}/title/${string}`; - /** The title of the manga. */ - title: string; - /** The title of the manga in other languages (alternative names) */ - altTitles?: string[]; /** * Manga cover or miniature. Some manga pages show the cover and the * banner, hence the use of the IImage interface. */ thumbnail?: IImage; - /** Synopsis or description of the manga */ - description?: string; - /** Indicates the status of the manga, in progress or completed. */ - status?: "ongoing" | "completed"; /** A list with the name of the authors of the manga */ authors?: string[]; - /** Genres manga */ - genres?: string[]; /** A list of the characters that are part of the history of manga */ characters?: string[]; @@ -113,8 +102,6 @@ export interface IManga { chapters?: IMangaChapter[]; /** A list of manga volumes */ volumes?: IMangaVolume[]; - /** Indicates if the content of the manga is for +18 */ - isNSFW: boolean; } /** @@ -126,15 +113,13 @@ export interface IManga { * * @author Zukaritasu */ -export interface IMangaResult { - /** {@inheritdoc IManga.id} */ +export interface IMangaResult extends IBaseResult { + /** Manga ID */ id: number | string; - /** {@inheritdoc IManga.title} */ - title: string; - /** {@inheritdoc IManga.thumbnail} */ + /** + * Manga cover or miniature. Some manga pages show the cover and the + * banner, hence the use of the IImage interface. */ thumbnail?: IImage; - /** {@inheritdoc IManga.url} */ - url: `/manga/${string}/title/${string}`; } //filter /** @@ -143,35 +128,21 @@ export interface IMangaResult { * * @author Zukaritasu */ -export class Manga implements IManga { - /** @inheritdoc */ - id: string; - /** @inheritdoc */ - url: `/manga/${string}/title/${string}`; - /** @inheritdoc */ - title: string; +export class MangaMedia extends BaseMedia implements IMangaMedia { /** @inheritdoc */ - altTitles?: string[]; + id: string | number; /** @inheritdoc */ thumbnail?: IImage; /** @inheritdoc */ - description?: string; - /** @inheritdoc */ - status?: "ongoing" | "completed"; - /** @inheritdoc */ authors?: string[]; /** @inheritdoc */ - langlist?: string[]; - /** @inheritdoc */ - genres?: string[]; - /** @inheritdoc */ characters?: string[]; /** @inheritdoc */ + langlist?: string[]; + /** @inheritdoc */ chapters?: IMangaChapter[]; /** @inheritdoc */ volumes?: IMangaVolume[]; - /** @inheritdoc */ - isNSFW: boolean; } //////////////////// From 5756046742d308bdfeeb3cf0d5f2a293b6e12117 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 25 May 2024 17:46:30 -0500 Subject: [PATCH 14/64] fix(properties)!: remove url from base interface BREAKING CHANGE: The 'url' property was removed from base interfaces and classes. Now, the 'url' property should be created individually for every interface. This allows us to not to set urls of wrong type of provider. --- src/types/anime.ts | 4 ++++ src/types/base.ts | 9 --------- src/types/manga.ts | 6 ++++++ src/types/search.ts | 4 ++++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/types/anime.ts b/src/types/anime.ts index 3e701391..8e82ed80 100644 --- a/src/types/anime.ts +++ b/src/types/anime.ts @@ -43,6 +43,8 @@ export interface IAnimeMedia extends IBaseMedia { * An IImage interface object representing the anime * image and its banner. */ image: IImage; + /** URL or location of the anime in the API. */ + url: `/anime/${string}/name/${string}` | string; /** Anime identifier that can be used when the anime name is not used in the URL. */ id?: number; /** @@ -109,6 +111,8 @@ export class AnimeMedia extends BaseMedia implements IAnimeMedia { /** @inheritdoc */ image: IImage; /** @inheritdoc */ + url: `/anime/${string}/name/${string}` | string; + /** @inheritdoc */ id?: number; /** @inheritdoc */ date?: IDatePeriod | ICalendar; diff --git a/src/types/base.ts b/src/types/base.ts index 484aa2e7..5ee0e20b 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -7,7 +7,6 @@ * * @author Victor */ -export type MediaURLOptions = "anime" | "manga"; /** * The base interface to create new types of providers. @@ -17,8 +16,6 @@ export type MediaURLOptions = "anime" | "manga"; export interface IBaseMedia { /** Name or title of the media */ name: string; - /** URL or location of the media in the API. */ - url: `/${MediaURLOptions}/${string}/name/${string}` | string; /** Alternative names or titles in different languages. */ alt_names?: string[] | string; /** Description or synopsis of the media. */ @@ -39,8 +36,6 @@ export interface IBaseMedia { export interface IBaseResult { /** Name of the media that was the result of your search */ name: string; - /** The media URL from the API */ - url: `/${MediaURLOptions}/${string}/name/${string}` | string; } /** @@ -52,8 +47,6 @@ export abstract class BaseMedia implements IBaseMedia { /** @inheritdoc */ name: string; /** @inheritdoc */ - url: `/${MediaURLOptions}/${string}/name/${string}` | string; - /** @inheritdoc */ alt_names?: string[] | string; /** @inheritdoc */ synopsis?: string; @@ -73,6 +66,4 @@ export abstract class BaseMedia implements IBaseMedia { export abstract class BaseResult implements IBaseResult { /** @inheritdoc */ name: string; - /** @inheritdoc */ - url: `/${MediaURLOptions}/${string}/name/${string}` | string; } diff --git a/src/types/manga.ts b/src/types/manga.ts index cba719e8..ecf4529c 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -83,6 +83,8 @@ export interface IMangaVolume { export interface IMangaMedia extends IBaseMedia { /** Manga ID */ id: number | string; + /** URL or location of the manga in the API. */ + url: `/manga/${string}/name/${string}` | string; /** * Manga cover or miniature. Some manga pages show the cover and the * banner, hence the use of the IImage interface. */ @@ -116,6 +118,8 @@ export interface IMangaMedia extends IBaseMedia { export interface IMangaResult extends IBaseResult { /** Manga ID */ id: number | string; + /** The manga URL from the API */ + url: `/manga/${string}/name/${string}` | string; /** * Manga cover or miniature. Some manga pages show the cover and the * banner, hence the use of the IImage interface. */ @@ -132,6 +136,8 @@ export class MangaMedia extends BaseMedia implements IMangaMedia { /** @inheritdoc */ id: string | number; /** @inheritdoc */ + url: `/manga/${string}/name/${string}` | string; + /** @inheritdoc */ thumbnail?: IImage; /** @inheritdoc */ authors?: string[]; diff --git a/src/types/search.ts b/src/types/search.ts index 539a2981..c98d7278 100644 --- a/src/types/search.ts +++ b/src/types/search.ts @@ -11,6 +11,8 @@ import { BaseResult, type IBaseResult } from "./base"; * @author Zukaritasu */ export interface IAnimeResult extends IBaseResult { + /** The anime URL from the API */ + url: `/anime/${string}/name/${string}` | string; /** The URL of the anime image */ image: string; /** @@ -59,6 +61,8 @@ export interface IResultSearch { * @author Zukaritasu */ export class AnimeResult extends BaseResult implements IAnimeResult { + /** @inheritdoc */ + url: `/anime/${string}/name/${string}` | string; /** @inheritdoc */ image: string; /** @inheritdoc */ From bc200794630644b008bb8e576de1020c0522106e Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 25 May 2024 17:48:30 -0500 Subject: [PATCH 15/64] chore: remove doc of unused base types --- src/types/base.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/types/base.ts b/src/types/base.ts index 5ee0e20b..413b50b5 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -1,13 +1,3 @@ -/** - * Here will stay all possible names of the types of providers. These options - * will be used to create a new URL of the media in the API. Modify it every - * time you need to add a new type of provider. - * - * Examples: `/anime/flv/...` or `/manga/comick/...` - * - * @author Victor - */ - /** * The base interface to create new types of providers. * From 70a19ee0d7aee8eb239c0e397ce963dbb90dfb9c Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 25 May 2024 21:29:08 -0500 Subject: [PATCH 16/64] feat(base)!: add base interface and class for chapters BREAKING CHANGE: Some properties of the new inherited chapters interfaces will change. --- src/types/base.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/types/base.ts b/src/types/base.ts index 413b50b5..240a05bb 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -1,3 +1,5 @@ +import type { IImage } from "./image"; + /** * The base interface to create new types of providers. * @@ -18,6 +20,23 @@ export interface IBaseMedia { nsfw?: boolean; } +/** + * The base interface to create new chapter classes for new types of providers. + * + * @author Victor + */ +export interface IBaseChapter { + /** + * Name of the media chapter. May contain the chapter number concatenated + * with the media name. + */ + name: string; + /** The chapter number. */ + num: number; + /** The image of the chapter shown as thumbnail */ + thumbnail?: IImage; +} + /** * The base interface to create new search results of any new type of provider. * @@ -57,3 +76,17 @@ export abstract class BaseResult implements IBaseResult { /** @inheritdoc */ name: string; } + +/** + * Extends the properties of a base chapter to create a new one. + * + * @author Victor + */ +export abstract class BaseChapter implements IBaseChapter { + /** @inheritdoc */ + name: string; + /** @inheritdoc */ + num: number; + /** @inheritdoc */ + thumbnail?: IImage; +} From 241b01588a591904aea3b35e5e5ba1acc37137f1 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 25 May 2024 21:34:12 -0500 Subject: [PATCH 17/64] feat(episode)!: add episode interface inherited from base BREAKING CHANGE: Some old properties of the episode interface was renamed to the base properties name. --- src/types/episode.ts | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/types/episode.ts b/src/types/episode.ts index 41e61eae..ca7c6190 100644 --- a/src/types/episode.ts +++ b/src/types/episode.ts @@ -1,5 +1,7 @@ //Spanish Providers - TypeScript version +import { BaseChapter, type IBaseChapter } from "./base"; + /** * This interface only puts the server name where host episode, * and url to that episode @@ -25,21 +27,13 @@ export interface IEpisodeServer { * * @author Zukaritasu */ -export interface IEpisode { - /** - * Name of anime episode. May contain the chapter number concatenated - * with the anime name. */ - name: string; +export interface IEpisode extends IBaseChapter { /** The episode URL in the API query */ url: `/anime/${string}/episode/${string | number}` | string; - /** The episode number. By default the value can be 0 in string or integer. */ - number: number | string; /** * List of available servers where the episode is located. Remember that * this is not the download link of the episode but of the video player. */ servers?: IEpisodeServer[]; - /** The image of the episode shown as thumbnail */ - image: string; } /** @@ -70,15 +64,9 @@ export class EpisodeServer implements IEpisodeServer { * * @author Zukaritasu */ -export class Episode implements IEpisode { - /** @inheritdoc */ - name: string; +export class Episode extends BaseChapter implements IEpisode { /** @inheritdoc */ url: `/anime/${string}/episode/${string | number}` | string; /** @inheritdoc */ - number: number | string; - /** @inheritdoc */ servers?: IEpisodeServer[] = []; - /** @inheritdoc */ - image: string; } From 6a7381c59a37fc5c45ee2f2be1babeeb5287eca0 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 25 May 2024 21:34:55 -0500 Subject: [PATCH 18/64] feat(MangaChapter)!: add manga chapter interface inherited from base BREAKING CHANGE: Some old properties of the Manga chapter interface was renamed to the base properties name. --- src/types/manga.ts | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/types/manga.ts b/src/types/manga.ts index ecf4529c..75d8be8b 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -1,7 +1,7 @@ import { IImage } from "./image"; import { ICalendar } from "./date"; import { IResultSearch } from "./search"; -import { BaseMedia, type IBaseMedia, type IBaseResult } from "./base"; +import { BaseChapter, BaseMedia, type IBaseChapter, type IBaseMedia, type IBaseResult } from "./base"; /** * The chapter is part of the manga and is also part of a volume. It is made @@ -9,25 +9,19 @@ import { BaseMedia, type IBaseMedia, type IBaseResult } from "./base"; * * @author Zukaritasu */ -export interface IMangaChapter { +export interface IMangaChapter extends IBaseChapter { /** ID or chapter identifier of the chapter that is part of the manga */ id: number | string; - /** Chapter title. May contain the manga chapter number. */ - title: string; - /** - * A brief description of what the new chapter brings. This property - * is optional because not all websites have it available. */ - description?: string; /** URL of the chapter in the API location */ url: `/manga/${string}/chapter/${string}`; - /** Chapter number */ - number: number; /** * Images of the manga chapter. * The first image may contain the cover of the chapter. */ images: string[]; - /** The cover page of the chapter. Refers to the first page of the chapter. */ - cover?: string; + /** + * A brief description of what the new chapter brings. This property + * is optional because not all websites have it available. */ + description?: string; /** * The date on which the chapter was published. This is optional because * in some cases it is not specified. */ @@ -159,22 +153,16 @@ export class MangaMedia extends BaseMedia implements IMangaMedia { * * @author Zukaritasu */ -export class MangaChapter implements IMangaChapter { +export class MangaChapter extends BaseChapter implements IMangaChapter { /** @inheritdoc */ id: number | string; /** @inheritdoc */ - title: string; - /** @inheritdoc */ description?: string; /** @inheritdoc */ url: `/manga/${string}/chapter/${string}`; /** @inheritdoc */ - number: number; - /** @inheritdoc */ images: string[] = []; /** @inheritdoc */ - cover?: string; - /** @inheritdoc */ date?: ICalendar; } From 068b1b9c9eb8dd84b3a0d94844801dfe51d31565 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sun, 26 May 2024 12:10:50 -0500 Subject: [PATCH 19/64] feat(BaseScraperModel): add generics extension This change allow us to only pass interfaces inherited from the base interfaces. --- src/models/BaseScraperModel.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/models/BaseScraperModel.ts b/src/models/BaseScraperModel.ts index ece424bd..4c8378ad 100644 --- a/src/models/BaseScraperModel.ts +++ b/src/models/BaseScraperModel.ts @@ -1,8 +1,9 @@ +import type { BaseMedia, BaseResult } from "@animetypes/base"; import type { IResultSearch } from "@animetypes/search"; -export abstract class BaseScraperModel { +export abstract class BaseScraperModel { public abstract readonly url: string; - public abstract GetItemInfo(item: string): Promise; - public abstract GetItemByFilter(...args: unknown[]): Promise>; + public abstract GetItemInfo(item: string): Promise; + public abstract GetItemByFilter(...args: unknown[]): Promise>; } From 319266aee1041bf853782da915872580ea4cf33a Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sun, 26 May 2024 12:11:44 -0500 Subject: [PATCH 20/64] feat(AnimeScraperModel): add extension to new anime interfaces --- src/models/AnimeScraperModel.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models/AnimeScraperModel.ts b/src/models/AnimeScraperModel.ts index 4b3a1e0c..f545bf7e 100644 --- a/src/models/AnimeScraperModel.ts +++ b/src/models/AnimeScraperModel.ts @@ -1,8 +1,8 @@ -import type { Anime } from "../types/anime"; -import type { IAnimeSearch } from "../types/search"; -import type { Episode } from "../types/episode"; +import type { AnimeMedia } from "@animetypes/anime"; +import type { Episode } from "@animetypes/episode"; +import type { IAnimeResult } from "@animetypes/search"; import { BaseScraperModel } from "./BaseScraperModel"; -export abstract class AnimeScraperModel extends BaseScraperModel { +export abstract class AnimeScraperModel extends BaseScraperModel { public abstract GetEpisodeServers(...args: unknown[]): Promise; } From b0c63c9d4f21731bde76049844b65343cb699386 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sun, 26 May 2024 12:12:08 -0500 Subject: [PATCH 21/64] feat(MangaScraperModel): add extension to new manga interfaces --- src/models/MangaScraperModel.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/MangaScraperModel.ts b/src/models/MangaScraperModel.ts index 548b2499..c648b5be 100644 --- a/src/models/MangaScraperModel.ts +++ b/src/models/MangaScraperModel.ts @@ -1,6 +1,6 @@ -import type { IMangaResult, Manga, MangaChapter, MangaVolume } from "@animetypes/manga"; +import type { MangaMedia, MangaChapter, MangaVolume, IMangaResult } from "@animetypes/manga"; import { BaseScraperModel } from "./BaseScraperModel"; -export abstract class MangaScraperModel extends BaseScraperModel { +export abstract class MangaScraperModel extends BaseScraperModel { public abstract GetMangaChapters(...args: unknown[]): Promise } From 6938ccaaf57e73d85a6e85a88a821545eb8c1a58 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sun, 26 May 2024 12:12:22 -0500 Subject: [PATCH 22/64] delete: mangaStructure file --- src/scraper/sites/manga/mangaStructure.ts | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 src/scraper/sites/manga/mangaStructure.ts diff --git a/src/scraper/sites/manga/mangaStructure.ts b/src/scraper/sites/manga/mangaStructure.ts deleted file mode 100644 index d3f45106..00000000 --- a/src/scraper/sites/manga/mangaStructure.ts +++ /dev/null @@ -1,11 +0,0 @@ -//example - -export class MangaProvider { - readonly url = "link"; - - async GetMangaInfo() {} - - async Filter() {} - - async GetMangaChapters() {} -} From d60ba03406390eeb2b33e467ccbc2f8aa38d8419 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Thu, 13 Jun 2024 17:50:44 -0500 Subject: [PATCH 23/64] feat(manga-types)!: add base inherited interface to IMangaVolume BREAKING CHANGE: Now, MangaVolume has been inherited from the base BaseChapter class. --- src/types/manga.ts | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/types/manga.ts b/src/types/manga.ts index 75d8be8b..d4405722 100644 --- a/src/types/manga.ts +++ b/src/types/manga.ts @@ -38,7 +38,7 @@ export interface IMangaChapter extends IBaseChapter { * * @author Zukaritasu */ -export interface IMangaVolume { +export interface IMangaVolume extends IBaseChapter { /** Manga volume ID */ id: number | string; /** @@ -46,24 +46,14 @@ export interface IMangaVolume { * the number of the first chapter of the volume is the beginning and * the last chapter is the end. */ range: [number, number]; - /** - * The title of the volume. The title may contain a short explanation - * of what the volume contains. */ - title?: string; /** * Description or introduction that explains a little of what is to * come in the next chapters that make up the volume. */ - description?: string; - /** Manga volume number */ - number?: number; //number + synopsis?: string; /** Images of the manga volume. */ images: string[]; /** The date on which the first chapter of the volume was published. */ date?: ICalendar; - /** - * The image or cover of the volume. - * This property contains the URL of the image */ - thumbnail?: string; /** URL of the volume in the API location */ url?: `/manga/${string}/volume/${string}`; // title or number } @@ -176,23 +166,17 @@ export class MangaChapter extends BaseChapter implements IMangaChapter { * * @author Zukaritasu */ -export class MangaVolume implements IMangaVolume { +export class MangaVolume extends BaseChapter implements IMangaVolume { /** @inheritdoc */ id: number | string; /** @inheritdoc */ range: [number, number] = [0, 0]; /** @inheritdoc */ - title?: string; - /** @inheritdoc */ - description?: string; - /** @inheritdoc */ - number?: number; //number + synopsis?: string; /** @inheritdoc */ images: string[]; /** @inheritdoc */ date?: ICalendar; /** @inheritdoc */ - thumbnail?: string; - /** @inheritdoc */ url?: `/manga/${string}/volume/${string}`; // title or number } From f8c5e67857e640e5d46f6a00b4494b31551d5e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 15:57:15 -0400 Subject: [PATCH 24/64] Fix AnimeLatinoHD add package CryptoJS for require scrapes --- package-lock.json | 6 ++ package.json | 1 + .../anime/animelatinohd/AnimeLatinoHD.ts | 56 +++++++++++++++---- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e76b13c..13d5724a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "axios": "^1.3.4", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "crypto-js": "^4.2.0", "express": "^4.18.2", "helmet": "^6.0.1", "morgan": "^1.10.0", @@ -4651,6 +4652,11 @@ "node": ">= 8" } }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, "node_modules/crypto-random-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", diff --git a/package.json b/package.json index 81d36340..8d06d548 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "axios": "^1.3.4", "cheerio": "^1.0.0-rc.12", "cors": "^2.8.5", + "crypto-js": "^4.2.0", "express": "^4.18.2", "helmet": "^6.0.1", "morgan": "^1.10.0", diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 31dce164..b1aa75ea 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -2,6 +2,8 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { Anime } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; +import CryptoJS from 'crypto-js' + import { AnimeSearch, ResultSearch, @@ -12,16 +14,15 @@ import { AnimeProviderModel } from "../../../ScraperAnimeModel"; export class AnimeLatinoHD extends AnimeProviderModel { readonly url = "https://www.animelatinohd.com"; - readonly api = "https://api.animelatinohd.com"; + readonly api = "https://web.animelatinohd.com"; + readonly key = "l7z8rIhQDXIH6pl66ZEQgPkNwkDlilgdOHMMWkxkzzE=" async GetAnimeInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = cheerio.load(data); - const animeInfoParseObj = JSON.parse($("#__NEXT_DATA__").html()).props - .pageProps.data; - + const animeInfoParseObj = JSON.parse(this.decrypt(JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data)); const AnimeInfo: Anime = { name: animeInfoParseObj.name, url: `/anime/animelatinohd/name/${anime}`, @@ -73,8 +74,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { const { data } = await axios.get(`${this.url}/ver/${anime}/${number}`); const $ = cheerio.load(data); - const animeEpisodeParseObj = JSON.parse($("#__NEXT_DATA__").html()).props - .pageProps.data; + const animeEpisodeParseObj = JSON.parse(this.decrypt(JSON.parse($("#__NEXT_DATA__").html()).props.pageProps.data)); const AnimeEpisodeInfo: Episode = { name: animeEpisodeParseObj.anime.name, @@ -102,12 +102,12 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( async (e: { server: { title: string }; id: string }) => { - + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(e.id))) const Server: EpisodeServer = { name: e.server.title, url: "", }; - Server.url = "https://api.animelatinohd.com/stream/" + e.id; + Server.url = `${warpVideo.request.res.responseUrl}`; Server.name = e.server.title; AnimeEpisodeInfo.servers.push(Server); @@ -120,7 +120,42 @@ export class AnimeLatinoHD extends AnimeProviderModel { console.log(error); } } + + decrypt(data: string){ + let t = CryptoJS.enc.Base64.parse(data).toString(CryptoJS.enc.Utf8) + t = JSON.parse(t) + const a = CryptoJS.enc.Base64.parse(t.iv) + const n = CryptoJS.AES.decrypt(t.value, CryptoJS.enc.Base64.parse(this.key), { + iv:a, + mode:CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + }) + return CryptoJS.enc.Utf8.stringify(n) + } + + encrypt(data:string | number){ + let t = CryptoJS.lib.WordArray.random(16) + let r + const a = CryptoJS.enc.Base64.parse(this.key) + const n = { + iv: t, + mode:CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7 + } + + const s = CryptoJS.AES.encrypt(data, a, n).toString(); + + r = { + iv: t = CryptoJS.enc.Base64.stringify(t), + value: s, + mac: CryptoJS.HmacSHA256(t + s, a).toString() + }; + r = JSON.stringify(r) + r = CryptoJS.enc.Utf8.parse(r) + + return CryptoJS.enc.Base64.stringify(r) + } async GetAnimeByFilter( search?: string, type?: number, @@ -129,6 +164,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { genre?: string ): Promise> { try { + const { data } = await axios.get(`${this.api}/api/anime/list`, { params: { search: search, @@ -138,8 +174,8 @@ export class AnimeLatinoHD extends AnimeProviderModel { page: page, }, }); - - const animeSearchParseObj = data; + + const animeSearchParseObj = JSON.parse(this.decrypt(data.data)); const animeSearch: ResultSearch = { nav: { From 91b08a2400cbae608f064ef93df5dc8fcafa1119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:09:17 -0400 Subject: [PATCH 25/64] Test warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index b1aa75ea..bbacee72 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -102,7 +102,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( async (e: { server: { title: string }; id: string }) => { - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(e.id))) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[0][1].id))) const Server: EpisodeServer = { name: e.server.title, url: "", From 2f68c0b748865ccbc15cc8bcf94252ee35a1b1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:21:50 -0400 Subject: [PATCH 26/64] Test 2 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index bbacee72..0249f00c 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -101,8 +101,9 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( - async (e: { server: { title: string }; id: string }) => { - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[0][1].id))) + async (e: { server: { title: string }; id: number }) => { + const id = JSON.stringify(e.id) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(id)) const Server: EpisodeServer = { name: e.server.title, url: "", @@ -114,7 +115,7 @@ export class AnimeLatinoHD extends AnimeProviderModel { } ) ); - + AnimeEpisodeInfo.servers.sort((a,b) => a.name.localeCompare(b.name)) return AnimeEpisodeInfo; } catch (error) { console.log(error); From 578bad2dd72d4d61e663ab5a88dc05b8b69bd5bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:39:44 -0400 Subject: [PATCH 27/64] test 3 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 0249f00c..4648b992 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -101,9 +101,9 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( - async (e: { server: { title: string }; id: number }) => { + async (e:{ id:number,server: { title: string } }) => { const id = JSON.stringify(e.id) - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(id)) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(id ? id : animeEpisodeParseObj.players[0][0].id)) const Server: EpisodeServer = { name: e.server.title, url: "", From 7c080375d2cf118ef078191769a0fac5962dd7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:42:39 -0400 Subject: [PATCH 28/64] test 4 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 4648b992..c29ee0c1 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -103,12 +103,12 @@ export class AnimeLatinoHD extends AnimeProviderModel { animeEpisodeParseObj.players[f_index].map( async (e:{ id:number,server: { title: string } }) => { const id = JSON.stringify(e.id) - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(id ? id : animeEpisodeParseObj.players[0][0].id)) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(animeEpisodeParseObj.players[0][0].id)) const Server: EpisodeServer = { name: e.server.title, url: "", }; - Server.url = `${warpVideo.request.res.responseUrl}`; + Server.url = `${id} - ${warpVideo.request.res.responseUrl}`; Server.name = e.server.title; AnimeEpisodeInfo.servers.push(Server); From a0034054a64ea31502798c6a0216061b1094bf59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:43:56 -0400 Subject: [PATCH 29/64] test 5 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index c29ee0c1..59e262a7 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -102,13 +102,13 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( async (e:{ id:number,server: { title: string } }) => { - const id = JSON.stringify(e.id) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(animeEpisodeParseObj.players[0][0].id)) const Server: EpisodeServer = { name: e.server.title, url: "", }; - Server.url = `${id} - ${warpVideo.request.res.responseUrl}`; + Server.url = `${warpVideo.request.res.responseUrl}`; Server.name = e.server.title; AnimeEpisodeInfo.servers.push(Server); From b14b3e64696d86ab8177bf3267ba79988ea8893b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:46:15 -0400 Subject: [PATCH 30/64] test 6 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 59e262a7..e709dca4 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -102,13 +102,13 @@ export class AnimeLatinoHD extends AnimeProviderModel { await Promise.all( animeEpisodeParseObj.players[f_index].map( async (e:{ id:number,server: { title: string } }) => { - - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(animeEpisodeParseObj.players[0][0].id)) + const id = JSON.stringify(e.id) + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[0][0].id))) const Server: EpisodeServer = { name: e.server.title, url: "", }; - Server.url = `${warpVideo.request.res.responseUrl}`; + Server.url = `${id} - ${warpVideo.request.res.responseUrl}`; Server.name = e.server.title; AnimeEpisodeInfo.servers.push(Server); From 8c32d55c7e8a334a8e262b4164754791ef070a7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:25:14 -0400 Subject: [PATCH 31/64] test 7 warpvideo in vercel deploy --- .../anime/animelatinohd/AnimeLatinoHD.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index e709dca4..26bf0f76 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -99,22 +99,20 @@ export class AnimeLatinoHD extends AnimeProviderModel { }); } - await Promise.all( - animeEpisodeParseObj.players[f_index].map( - async (e:{ id:number,server: { title: string } }) => { - const id = JSON.stringify(e.id) - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[0][0].id))) + + for (let index = 0; index < animeEpisodeParseObj.players[f_index].length; index++) { + const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[f_index][index].id))) const Server: EpisodeServer = { - name: e.server.title, + name: animeEpisodeParseObj.players[f_index][index].server.title, url: "", }; - Server.url = `${id} - ${warpVideo.request.res.responseUrl}`; - Server.name = e.server.title; + Server.url = `${animeEpisodeParseObj.players[f_index][index].id} - ${warpVideo.request.res.responseUrl}`; + Server.name = animeEpisodeParseObj.players[f_index][index].server.title; - AnimeEpisodeInfo.servers.push(Server); - } - ) - ); + AnimeEpisodeInfo.servers.push(Server); + } + + AnimeEpisodeInfo.servers.sort((a,b) => a.name.localeCompare(b.name)) return AnimeEpisodeInfo; } catch (error) { From 79d2ec7c9c56bf08ac7557bbfae64091766eed68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:28:11 -0400 Subject: [PATCH 32/64] test 8 warpvideo in vercel deploy --- src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index 26bf0f76..91a49b28 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -101,12 +101,12 @@ export class AnimeLatinoHD extends AnimeProviderModel { for (let index = 0; index < animeEpisodeParseObj.players[f_index].length; index++) { - const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[f_index][index].id))) + //const warpVideo = await axios.get(this.api +'/video/'+this.encrypt(JSON.stringify(animeEpisodeParseObj.players[f_index][index].id))) const Server: EpisodeServer = { name: animeEpisodeParseObj.players[f_index][index].server.title, url: "", }; - Server.url = `${animeEpisodeParseObj.players[f_index][index].id} - ${warpVideo.request.res.responseUrl}`; + Server.url = `${this.api}/video/${this.encrypt(JSON.stringify(animeEpisodeParseObj.players[f_index][index].id))}`; Server.name = animeEpisodeParseObj.players[f_index][index].server.title; AnimeEpisodeInfo.servers.push(Server); From 388379774e142e44a82fa0664158771da55b9fdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:11:20 -0400 Subject: [PATCH 33/64] Update Providers to new class --- .../sites/anime/animeBlixs/AnimeBlix.ts | 23 +++++++-------- .../anime/animelatinohd/AnimeLatinoHD.ts | 17 ++++------- .../sites/anime/wcostream/WcoStream.ts | 29 +++++++++---------- 3 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index f78dd095..88a08f42 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -1,12 +1,11 @@ import * as cheerio from "cheerio"; import axios from "axios"; -import { Anime } from "../../../../types/anime"; +import { AnimeMedia } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { - AnimeSearch, ResultSearch, type IResultSearch, - type IAnimeSearch, + type IAnimeResult, } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; //import { Calendar } from "@animetypes/date"; @@ -24,7 +23,7 @@ export class AnimeBlix extends AnimeScraperModel { readonly url = "https://vwv.animeblix.org"; readonly api = "https://api.animelatinohd.com"; - async GetItemInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get( `${this.url}/animes/${anime.includes("ver-") ? anime : "ver-" + anime}` @@ -76,11 +75,11 @@ export class AnimeBlix extends AnimeScraperModel { } const AltNames = AcceptAlts.slice(0, AltsSlice); - const AnimeInfo: Anime = { + const AnimeInfo: AnimeMedia = { name: $(".cn .ti h1 strong").text(), url: `/anime/animeblix/name/${anime}`, synopsis: $(".cn .info .r .tx .content p").first().text(), - alt_name: [...AltNames.split("---")], + alt_names: [...AltNames.split("---")], image: { url: $(".cn .info .l .i img").attr("data-src"), }, @@ -116,8 +115,7 @@ export class AnimeBlix extends AnimeScraperModel { ListEpisode.map((e) => { const AnimeEpisode: Episode = { name: "Episode " + e, - number: e, - image: "", + num: Number(e), url: `/anime/animeblix/episode/${anime + "-" + e}`, }; @@ -142,8 +140,7 @@ export class AnimeBlix extends AnimeScraperModel { const AnimeEpisodeInfo: Episode = { name: number, url: `/anime/animeblix/episode/${episode}`, - number: number, - image: "", + num: Number(number), servers: [], }; @@ -171,7 +168,7 @@ export class AnimeBlix extends AnimeScraperModel { page?: number, year?: string, genre?: string - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { params: { @@ -185,7 +182,7 @@ export class AnimeBlix extends AnimeScraperModel { const animeSearchParseObj = data; - const animeSearch: ResultSearch = { + const animeSearch: ResultSearch = { nav: { count: animeSearchParseObj.data.length, current: animeSearchParseObj.current_page, @@ -198,7 +195,7 @@ export class AnimeBlix extends AnimeScraperModel { results: [], }; animeSearchParseObj.data.map((e) => { - const animeSearchData: AnimeSearch = { + const animeSearchData: IAnimeResult = { name: e.name, image: "https://www.themoviedb.org/t/p/original" + diff --git a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts index c2ce8474..c199150a 100644 --- a/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts +++ b/src/scraper/sites/anime/animelatinohd/AnimeLatinoHD.ts @@ -6,17 +6,12 @@ import CryptoJS from 'crypto-js' import { ResultSearch, - type IResultSearch, - type IAnimeResult, + AnimeResult } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; export class AnimeLatinoHD extends AnimeScraperModel { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public GetItemByFilter(..._args: unknown[]): Promise> { - throw new Error("Method not implemented."); - } - + readonly url = "https://www.animelatinohd.com"; readonly api = "https://web.animelatinohd.com"; readonly key = "l7z8rIhQDXIH6pl66ZEQgPkNwkDlilgdOHMMWkxkzzE=" @@ -155,13 +150,13 @@ export class AnimeLatinoHD extends AnimeScraperModel { return CryptoJS.enc.Base64.stringify(r) } - async GetAnimeByFilter( + async GetItemByFilter( search?: string, type?: number, page?: number, year?: string, genre?: string - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { @@ -176,7 +171,7 @@ export class AnimeLatinoHD extends AnimeScraperModel { const animeSearchParseObj = JSON.parse(this.decrypt(data.data)); - const animeSearch: ResultSearch = { + const animeSearch: ResultSearch = { nav: { count: animeSearchParseObj.data.length, current: animeSearchParseObj.current_page, @@ -189,7 +184,7 @@ export class AnimeLatinoHD extends AnimeScraperModel { results: [], }; animeSearchParseObj.data.map((e) => { - const animeSearchData: IAnimeResult = { + const animeSearchData: AnimeResult = { name: e.name, image: "https://www.themoviedb.org/t/p/original" + diff --git a/src/scraper/sites/anime/wcostream/WcoStream.ts b/src/scraper/sites/anime/wcostream/WcoStream.ts index fabffe21..9f2056b0 100644 --- a/src/scraper/sites/anime/wcostream/WcoStream.ts +++ b/src/scraper/sites/anime/wcostream/WcoStream.ts @@ -1,12 +1,10 @@ import * as cheerio from "cheerio"; import axios from "axios"; -import { Anime } from "../../../../types/anime"; +import { AnimeMedia } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { - type IResultSearch, - type IAnimeSearch, ResultSearch, - AnimeSearch, + AnimeResult } from "../../../../types/search"; import { UnPacked } from "../../../../types/utils"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; @@ -39,7 +37,7 @@ axios.defaults.headers.common["User-Agent"] = export class WcoStream extends AnimeScraperModel { readonly url = "https://www.wcostream.tv"; - async GetItemInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`, { headers: { @@ -62,7 +60,7 @@ export class WcoStream extends AnimeScraperModel { .replace("Genre;", "") .replace("Language; ", ""); - const AnimeInfo: Anime = { + const AnimeInfo: AnimeMedia = { name: name, url: `/anime/wcostream/name/${anime}`, synopsis: $("#category_description .ui-grid-solo .ui-block-a div p") @@ -110,14 +108,14 @@ export class WcoStream extends AnimeScraperModel { if (data && !data.includes("Movie") && !data.includes("OVA")) { const AnimeEpisode: Episode = { name: data, - number: episode, - image: `https://cdn.animationexplore.com/thumbs/${$(e) + num: Number(episode), + thumbnail: {url:`https://cdn.animationexplore.com/thumbs/${$(e) .find("a") .attr("href") .replace("https://www.wcostream.tv/", "") .replace("/", "") .replace(/[^a-zA-Z0-9 ]/g, " ") - .replace(/\s+/g, "-")}.jpg`, + .replace(/\s+/g, "-")}.jpg`}, url: `/anime/wcostream/episode/${ anime.replace(/[^a-zA-Z0-9 ]/g, " ").replace(/\s+/g, "-") + "-" + @@ -170,8 +168,7 @@ export class WcoStream extends AnimeScraperModel { url: `/anime/wcostream/episode/${episode}${ season ? "?season=" + season : "" }`, - number: NumEpisode, - image: "", + num: Number(NumEpisode), servers: [], }; @@ -187,7 +184,7 @@ export class WcoStream extends AnimeScraperModel { .replace("", "") .trim(); - AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text(); + AnimeEpisodeInfo.thumbnail.url = $$(e).find("jwplayer[type='image']").text(); const Server: EpisodeServer = { name: "JWplayer - " + @@ -204,7 +201,7 @@ export class WcoStream extends AnimeScraperModel { .replace("", "") .trim(); - AnimeEpisodeInfo.image = $$(e).find("jwplayer[type='image']").text(); + AnimeEpisodeInfo.thumbnail.url = $$(e).find("jwplayer[type='image']").text(); const Server: EpisodeServer = { name: @@ -225,7 +222,7 @@ export class WcoStream extends AnimeScraperModel { async GetItemByFilter( search?: string, page?: number - ): Promise> { + ): Promise> { try { const formdata = new FormData(); formdata.append("catara", search); @@ -234,7 +231,7 @@ export class WcoStream extends AnimeScraperModel { const { data } = await axios.post(`${this.url}/search`, formdata); const $ = cheerio.load(data); - const animeSearch: ResultSearch = { + const animeSearch: ResultSearch = { nav: { count: $("#blog .cerceve").length, current: Number(page ? page : 1), @@ -252,7 +249,7 @@ export class WcoStream extends AnimeScraperModel { (animeSearch.nav.current > 1 ? i + 1 : i) <= 28 * animeSearch.nav.current ) { - const animeSearchData: AnimeSearch = { + const animeSearchData: AnimeResult = { name: $(e).find(".iccerceve a").attr("title"), image: $(e).find(".iccerceve a img").attr("src"), url: `/anime/wcostream/name/${$(e) From c23786b5af2e76b001be380c44d75aa544468fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:12:18 -0400 Subject: [PATCH 34/64] AnimeBlix to direct class --- src/scraper/sites/anime/animeBlixs/AnimeBlix.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts index 88a08f42..84763c0b 100644 --- a/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts +++ b/src/scraper/sites/anime/animeBlixs/AnimeBlix.ts @@ -4,8 +4,7 @@ import { AnimeMedia } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { ResultSearch, - type IResultSearch, - type IAnimeResult, + AnimeResult } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; //import { Calendar } from "@animetypes/date"; @@ -168,7 +167,7 @@ export class AnimeBlix extends AnimeScraperModel { page?: number, year?: string, genre?: string - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.api}/api/anime/list`, { params: { @@ -182,7 +181,7 @@ export class AnimeBlix extends AnimeScraperModel { const animeSearchParseObj = data; - const animeSearch: ResultSearch = { + const animeSearch: ResultSearch = { nav: { count: animeSearchParseObj.data.length, current: animeSearchParseObj.current_page, @@ -195,7 +194,7 @@ export class AnimeBlix extends AnimeScraperModel { results: [], }; animeSearchParseObj.data.map((e) => { - const animeSearchData: IAnimeResult = { + const animeSearchData: AnimeResult = { name: e.name, image: "https://www.themoviedb.org/t/p/original" + From 708cbbdae83338c124b62a357a5ae296bb65039a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:48:43 -0400 Subject: [PATCH 35/64] comick,Inmanga to direct class --- src/scraper/sites/manga/comick/Comick.ts | 39 ++++++++++------------ src/scraper/sites/manga/inmanga/Inmanga.ts | 33 +++++++++--------- 2 files changed, 33 insertions(+), 39 deletions(-) diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index 186dc7a9..37936924 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -1,7 +1,7 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { - Manga, + MangaMedia, MangaChapter, type IMangaResult, } from "../../../../types/manga"; @@ -35,7 +35,7 @@ export class Comick { year?: number, genre?: string, page?: number - ) { + ): Promise> { try { const { data } = await axios.get(`${this.api}/v1.0/search`, { params: { @@ -66,7 +66,7 @@ export class Comick { }) => { const ListMangaResult: IMangaResult = { id: e.id, - title: e.title, + name: e.title, thumbnail: { url: "https://meo.comick.pictures/" + e.md_covers[0].b2key, }, @@ -82,7 +82,7 @@ export class Comick { } } - async GetMangaInfo(manga: string, lang: string): Promise { + async GetMangaInfo(manga: string, lang: string): Promise { try { const { data } = await axios.get( `${this.url}/comic/${manga}` @@ -98,15 +98,15 @@ export class Comick { `${this.url}/_next/data/${buildId}/comic/${manga}/${mangaInfoParseObj.firstChap.hid + "-chapter-" + mangaInfoParseObj.firstChap.chap + "-" + mangaInfoParseObj.firstChap.lang}.json` ); } - const MangaInfo: Manga = { + const MangaInfo: MangaMedia = { id: mangaInfoParseObj.comic.id, - title: mangaInfoParseObj.comic.title, - altTitles: mangaInfoParseObj.comic.md_titles.map( + name: mangaInfoParseObj.comic.title, + alt_names: mangaInfoParseObj.comic.md_titles.map( (e: { title: string }) => e.title ), url: `/manga/comick/title/${mangaInfoParseObj.comic.slug}`, - description: mangaInfoParseObj.comic.desc, - isNSFW: mangaInfoParseObj.comic.hentai, + synopsis: mangaInfoParseObj.comic.desc, + nsfw: mangaInfoParseObj.comic.hentai, langlist: mangaInfoParseObj.langList, status: mangaInfoParseObj.comic.status == "1" ? "ongoing" : "completed", authors: mangaInfoParseObj.authors.map((e: { name: string }) => e.name), @@ -135,12 +135,11 @@ export class Comick { const MangaInfoChapter: MangaChapter = { id: e.id, - title: e.title, + name: e.title, url: `/manga/comick/chapter/${e.hid}-${mangaInfoParseObj.comic.slug }-${e.chap ? e.chap : "err"}${langChapter}`, - number: e.chap, + num: Number(e.chap), images: null, - cover: null, date: { year: mindate.getFullYear() ? mindate.getFullYear() : null, month: mindate.getMonth() ? mindate.getMonth() : null, @@ -187,9 +186,9 @@ export class Comick { const MangaChapterInfoChapter: MangaChapter = { id: mangaChapterInfoParseObj.chapter.id, - title: mangaChapterInfoParseObj.seoTitle, + name: mangaChapterInfoParseObj.seoTitle, url: `/manga/comick/chapter/${manga}`, - number: mangaChapterInfoParseObj.chapter.chap, + num: mangaChapterInfoParseObj.chapter.chap, images: mangaChapterInfoParseObj.chapter.md_images.map( (e: { w: number; h: number; name: string; b2key: string }) => { return { @@ -200,9 +199,7 @@ export class Comick { }; } ), - cover: - "https://meo.comick.pictures/" + - mangaChapterInfoParseObj.chapter.md_comics.md_covers[0].b2key, + thumbnail:{url:null,banner:"https://meo.comick.pictures/" +mangaChapterInfoParseObj.chapter.md_comics.md_covers[0].b2key}, date: { year: mindate.getFullYear() ? mindate.getFullYear() : null, month: mindate.getMonth() ? mindate.getMonth() : null, @@ -224,9 +221,9 @@ export class Comick { const MangaChapterInfoChapter: MangaChapter = { id: dataBuild.data.pageProps.chapter.id, - title: dataBuild.data.pageProps.seoTitle, + name: dataBuild.data.pageProps.seoTitle, url: `/manga/comick/chapter/${manga}`, - number: dataBuild.data.pageProps.chapter.chap, + num: dataBuild.data.pageProps.chapter.chap, images: dataBuild.data.pageProps.chapter.md_images.map( (s: { w: number; h: number; name: string; b2key: string }) => { return { @@ -237,9 +234,7 @@ export class Comick { }; } ), - cover: - "https://meo.comick.pictures/" + - dataBuild.data.pageProps.chapter.md_comics.md_covers[0].b2key, + thumbnail:{url:null,banner:"https://meo.comick.pictures/" +dataBuild.data.pageProps.chapter.md_comics.md_covers[0].b2key}, date: { year: mindate.getFullYear() ? mindate.getFullYear() : null, month: mindate.getMonth() ? mindate.getMonth() : null, diff --git a/src/scraper/sites/manga/inmanga/Inmanga.ts b/src/scraper/sites/manga/inmanga/Inmanga.ts index 3e7e7d36..1098a38e 100644 --- a/src/scraper/sites/manga/inmanga/Inmanga.ts +++ b/src/scraper/sites/manga/inmanga/Inmanga.ts @@ -1,7 +1,7 @@ import * as cheerio from "cheerio"; import axios from "axios"; import { - Manga, + MangaMedia, MangaChapter, type IMangaResult, } from "../../../../types/manga"; @@ -117,7 +117,7 @@ export class Inmanga { const ListMangaResult: IMangaResult = { id: null, - title: title, + name: title, thumbnail: { url: `https://inmanga.com/thumbnails/manga/${name}/${cid}`, }, @@ -134,22 +134,23 @@ export class Inmanga { } } - async GetMangaInfo(manga: string, cid: string): Promise { + async GetMangaInfo(manga: string, cid: string): Promise { try { const dataPost = await axios.get(`${this.url}/ver/manga/${manga}/${cid}`); const $_ = cheerio.load(dataPost.data); + const AltNames = [] - const MangaInfo: Manga = { + const MangaInfo: MangaMedia = { id: cid, - title: $_("div.col-md-3.col-sm-4 div.panel-heading.visible-xs").text(), - altTitles: [], + name: $_("div.col-md-3.col-sm-4 div.panel-heading.visible-xs").text(), + alt_names: AltNames, url: `/manga/inmanga/title/${manga}`, - description: $_( + synopsis: $_( "body > div > section > div > div > div:nth-child(6) > div > div.panel-body" ) .text() .trim(), - isNSFW: false, + nsfw: false, status: $_(".col-md-3.col-sm-4 .list-group > a:nth-child(1) > span").text() == "En emisión" @@ -162,17 +163,17 @@ export class Inmanga { url: `https://inmanga.com/thumbnails/manga/${manga}/${cid}`, }, }; - $_( ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .text-muted span" ).each((_i, e) => - MangaInfo.altTitles.push($_(e).text().replace(";", "")) + AltNames.push($_(e).text().replace(";", "")) ); + $_( ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm" ).each((_i, e) => MangaInfo.genres.push($_(e).text().trim())); - MangaInfo.altTitles.slice(MangaInfo.altTitles.indexOf('""'), 0); + MangaInfo.alt_names.slice(MangaInfo.alt_names.indexOf('""'), 0); MangaInfo.genres.slice(MangaInfo.genres.indexOf('""'), 0); const dataChPost = await axios.get( @@ -188,11 +189,10 @@ export class Inmanga { }) => { const MangaInfoChapter: MangaChapter = { id: e.Id, - title: e.MangaName, + name: e.MangaName, url: `/manga/inmanga/chapter/${manga}-${e.Number}?cid=${e.Identification}`, // Change url (: = title ) manga.replace(/[^a-zA-Z:]/g," ") - number: e.Number, + num: e.Number, images: null, - cover: null, date: { year: null, month: null, @@ -223,11 +223,10 @@ export class Inmanga { const MangaChapterInfoChapter: MangaChapter = { id: 1, - title: "", + name: "", url: `/manga/inmanga/chapter/`, - number: idNumber, + num: idNumber, images: allimages, - cover: null, date: { year: null, month: null, From e6f2c50dc7300a4679e46bea22383dfd1717ad68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:50:49 -0400 Subject: [PATCH 36/64] Animevostfr to direct class --- src/scraper/sites/anime/animevostfr/Animevostfr.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/scraper/sites/anime/animevostfr/Animevostfr.ts b/src/scraper/sites/anime/animevostfr/Animevostfr.ts index b183e2ec..8901921e 100644 --- a/src/scraper/sites/anime/animevostfr/Animevostfr.ts +++ b/src/scraper/sites/anime/animevostfr/Animevostfr.ts @@ -4,8 +4,7 @@ import { AnimeMedia } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { ResultSearch, - type IResultSearch, - type IAnimeResult, + AnimeResult, } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; @@ -158,7 +157,7 @@ export class Animevostfr extends AnimeScraperModel { async GetItemByFilter( search?: string, page?: number - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.url}/page/${page ? page : 1}`, { params: { @@ -168,7 +167,7 @@ export class Animevostfr extends AnimeScraperModel { const $ = cheerio.load(data); - const animeSearch: ResultSearch = { + const animeSearch: ResultSearch = { nav: { count: $(".movies-list .ml-item").length, current: page ? Number(page) : 1, @@ -182,7 +181,7 @@ export class Animevostfr extends AnimeScraperModel { }; $(".movies-list .ml-item").each((_i, e) => { - const animeSearchData: IAnimeResult = { + const animeSearchData: AnimeResult = { name: $(e).find(".mli-info").text(), image: $(e).find(".mli-thumb").attr("data-original"), url: `/anime/animevostfr/name/${$(e).find(".ml-mask").attr("href").replace(this.url, "").replace("/", "").replace("/", "")}`, From 0641cbf1d06b0a5fa815d3ce137d2fd6a484d6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Sat, 22 Jun 2024 11:56:09 -0400 Subject: [PATCH 37/64] Fix: unit tests(some providers) --- src/scraper/sites/manga/inmanga/Inmanga.ts | 2 +- src/test/Animelatinohd.spec.ts | 8 ++++---- src/test/Comick.spec.ts | 4 ++-- src/test/Inmanga.spec.ts | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/scraper/sites/manga/inmanga/Inmanga.ts b/src/scraper/sites/manga/inmanga/Inmanga.ts index 1098a38e..fc35f3d1 100644 --- a/src/scraper/sites/manga/inmanga/Inmanga.ts +++ b/src/scraper/sites/manga/inmanga/Inmanga.ts @@ -15,7 +15,7 @@ axios.defaults.headers.common["User-Agent"] = export class Inmanga { readonly url = "https://inmanga.com"; - async GetMangaByFilter(search?: string, type?: number, genre?: string[]) { + async GetMangaByFilter(search?: string, type?: number, genre?: string[]): Promise> { try { const formdata = new FormData(); formdata.append("filter[queryString]", search); diff --git a/src/test/Animelatinohd.spec.ts b/src/test/Animelatinohd.spec.ts index 455b6de2..9776efc1 100644 --- a/src/test/Animelatinohd.spec.ts +++ b/src/test/Animelatinohd.spec.ts @@ -10,12 +10,12 @@ describe("AnimeLatinohd", () => { it("should get anime info successfully", async () => { const animeInfo = await animelatinohd.GetItemInfo("wonder-egg-priority"); expect(animeInfo.name).toBe("Wonder Egg Priority"); - expect(animeInfo.alt_name).toContain("ワンダーエッグ・プライオリティ"); + expect(animeInfo.alt_names).toContain("ワンダーエッグ・プライオリティ"); expect(animeInfo.image.url).toContain(".jpg"); expect(animeInfo.status).toBe("Finalizado"); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - expect(animeInfo.episodes.length).toBeGreaterThan(0); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); }); it("should filter anime successfully", async () => { diff --git a/src/test/Comick.spec.ts b/src/test/Comick.spec.ts index 98624d1d..b4ac97ea 100644 --- a/src/test/Comick.spec.ts +++ b/src/test/Comick.spec.ts @@ -10,8 +10,8 @@ describe("Comick", () => { it("should get anime info successfully", async () => { const mangaInfo = await comick.GetMangaInfo("00-solo-leveling", "en"); - expect(mangaInfo.title).toBe("Solo Leveling"); - expect(mangaInfo.altTitles).toContain("我独自升级"); + expect(mangaInfo.name).toBe("Solo Leveling"); + expect(mangaInfo.alt_names).toContain("我独自升级"); expect(mangaInfo.status).toBe("completed"); }); diff --git a/src/test/Inmanga.spec.ts b/src/test/Inmanga.spec.ts index c6ae2eda..ff3fc84d 100644 --- a/src/test/Inmanga.spec.ts +++ b/src/test/Inmanga.spec.ts @@ -8,10 +8,10 @@ describe("Inmanga", () => { }); it("should get anime info successfully", async () => { - const mangaInfo = await inmanga.GetMangaInfo("Kimetsu-no-Yaiba"); + const mangaInfo = await inmanga.GetMangaInfo("Kimetsu-no-Yaiba","78352626-0e2c-4b10-9610-28abf57c6881"); - expect(mangaInfo.title).toBe("Kimetsu no Yaiba"); - expect(mangaInfo.altTitles).toContain("Blade of Demon Destruction"); + expect(mangaInfo.name).toBe("Kimetsu no Yaiba"); + expect(mangaInfo.alt_names).toContain("Blade of Demon Destruction"); expect(mangaInfo.status).toBe("ongoing"); }); From 094c01b1a82c7f4b1d18103a9b2a63d0b17ba747 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 22 Jun 2024 16:24:57 -0500 Subject: [PATCH 38/64] fix(manganelo): class implementation and tests --- .../v1/manga/manganelo/ManganeloRoutes.ts | 4 +-- .../sites/manga/manganelo/Manganelo.ts | 31 ++++++++++--------- src/test/Manganelo.spec.ts | 12 +++---- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts index 93de587e..2716a7ba 100644 --- a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts +++ b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts @@ -6,7 +6,7 @@ const router = Router(); const manganelo = new Manganelo(); router.get(`/manga/${manganelo.name}/title/:id`, async (req, res) => { - const result = await manganelo.GetMangaInfo( + const result = await manganelo.GetItemInfo( req.params.id as unknown as string, ); @@ -14,7 +14,7 @@ router.get(`/manga/${manganelo.name}/title/:id`, async (req, res) => { }); router.get(`/manga/${manganelo.name}/filter`, async (req, res) => { - const result = await manganelo.Filter({ + const result = await manganelo.GetItemByFilter({ sts: req.query.status as unknown as "ongoing" | "completed", genres: req.query.genres as unknown as string, orby: req.query diff --git a/src/scraper/sites/manga/manganelo/Manganelo.ts b/src/scraper/sites/manga/manganelo/Manganelo.ts index 5e8f0c01..aff80ac5 100644 --- a/src/scraper/sites/manga/manganelo/Manganelo.ts +++ b/src/scraper/sites/manga/manganelo/Manganelo.ts @@ -1,13 +1,14 @@ -import { IMangaResult, Manga, MangaChapter } from "../../../../types/manga"; +import { IMangaResult, MangaMedia, MangaChapter } from "../../../../types/manga"; import axios from "axios"; import { load } from "cheerio"; import { Image } from "../../../../types/image"; import { ManganatoManagerUtils } from "./ManganatoManagerUtils"; import { type IManganatoFilterParams } from "./ManganatoTypes"; import { ResultSearch } from "../../../../types/search"; +import { MangaScraperModel } from "../../../../models/MangaScraperModel"; -export class Manganelo { - private readonly url = "https://manganelo.tv"; //chapmanganelo.com //mangakakalot.tv; +export class Manganelo extends MangaScraperModel { + readonly url = "https://manganelo.tv"; //chapmanganelo.com //mangakakalot.tv; readonly name = "manganelo"; private readonly manager = ManganatoManagerUtils.Instance; @@ -102,7 +103,7 @@ export class Manganelo { const mangaInfoResults: IMangaResult = { id: mangaResultId, - title: name, + name: name, url: `/manga/${this.name}/title/${mangaResultId}`, }; @@ -111,11 +112,11 @@ export class Manganelo { .get(); } - async GetMangaInfo(mangaId: string) { + async GetItemInfo(mangaId: string) { const { data } = await axios.get(`${this.url}/manga/manga-${mangaId}`); const $ = load(data); - const manga = new Manga(); + const manga = new MangaMedia(); const title = $("div.panel-story-info > div.story-info-right > h1") .text() @@ -141,9 +142,9 @@ export class Manganelo { const chapterId = url.substring(url.lastIndexOf("-") + 1); chapter.id = Number(chapterId); - chapter.title = $(element).find("a.chapter-name").text().trim(); + chapter.name = $(element).find("a.chapter-name").text().trim(); chapter.url = `/manga/${this.name}/chapter/${mangaId}?num=${chapterId}`; - chapter.number = Number(chapterId); + chapter.num = Number(chapterId); chapter.images = null; return chapter; @@ -152,22 +153,22 @@ export class Manganelo { manga.id = mangaId; manga.url = `/manga/${this.name}/title/${mangaId}`; - manga.title = title; - manga.altTitles = Array.of(altTitle); + manga.name = title; + manga.alt_names = Array.of(altTitle); manga.thumbnail = new Image(thumbnail); - manga.description = description; + manga.synopsis = description; manga.status = status; manga.authors = authors; manga.genres = genres; manga.characters = null; manga.chapters = chapters; manga.volumes = null; - manga.isNSFW = this.isNsfw(genres); + manga.nsfw = this.isNsfw(genres); return manga; } - async Filter(params: IManganatoFilterParams) { + async GetItemByFilter(params: IManganatoFilterParams) { const url = this.manager.url.generate(params); const { data } = await axios.get(url); @@ -193,9 +194,9 @@ export class Manganelo { const chapter = new MangaChapter(); chapter.id = Number(chapterNumber); - chapter.title = name; + chapter.name = name; chapter.url = `/manga/${this.name}/chapter/${mangaId}?num=${chapterNumber}`; - chapter.number = Number(chapterNumber); + chapter.num = Number(chapterNumber); chapter.images = images; return chapter; diff --git a/src/test/Manganelo.spec.ts b/src/test/Manganelo.spec.ts index 4a5125a2..789d6dc3 100644 --- a/src/test/Manganelo.spec.ts +++ b/src/test/Manganelo.spec.ts @@ -59,17 +59,17 @@ describe("Manganelo", () => { ]; testsSuites.forEach(async (options) => { - const mangaInfo = await manganelo.GetMangaInfo(options.id); - expect(mangaInfo.title).toStrictEqual(options.title); + const mangaInfo = await manganelo.GetItemInfo(options.id); + expect(mangaInfo.name).toStrictEqual(options.title); - if (mangaInfo.altTitles) - expect(mangaInfo.altTitles.length).toBeGreaterThanOrEqual(1); + if (mangaInfo.alt_names) + expect(mangaInfo.alt_names.length).toBeGreaterThanOrEqual(1); if (mangaInfo.thumbnail && mangaInfo.thumbnail.url) expect(mangaInfo.thumbnail.url).toContain(".jpg"); expect(mangaInfo.status).toStrictEqual(options.status); - expect(mangaInfo.isNSFW).toStrictEqual(options.nsfw); + expect(mangaInfo.nsfw).toStrictEqual(options.nsfw); if (mangaInfo.genres) expect(mangaInfo.genres.length).toBeGreaterThanOrEqual(1); @@ -96,7 +96,7 @@ describe("Manganelo", () => { ]; filterTestsSuites.forEach(async (options) => { - const result = await manganelo.Filter({ + const result = await manganelo.GetItemByFilter({ genres: options.genres.join(" "), orby: options.orby, page: options.page, From e13f9e2982beaec6984284a049bb258dcd2381ae Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 22 Jun 2024 16:25:43 -0500 Subject: [PATCH 39/64] fix(mangareader): class implementation and tests --- .../v1/manga/mangareader/MangaReaderRoutes.ts | 10 ++--- .../sites/manga/MangaReader/MangaReader.ts | 45 ++++++++++--------- src/test/MangaReader.spec.ts | 32 ++++++------- 3 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts index 359ebdef..04419ff5 100644 --- a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts +++ b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts @@ -14,9 +14,9 @@ const router = Router(); router.get("/manga/mangareader/title/:id", async (req, res) => { try { - const id = req.params.id as unknown as number; + const id = req.params.id as unknown as string; - const data = await mangaReader.GetMangaInfo(id); + const data = await mangaReader.GetItemInfo(id); return res.status(200).send(data); } catch (e) { @@ -42,7 +42,7 @@ router.get("/manga/mangareader/filter", async (req, res) => { const sort = req.query.sort as MangaReaderFilterSort; const numPage = req.query.page as unknown as number; - const data = await mangaReader.Filter({ + const data = await mangaReader.GetItemByFilter({ type, status, ratingType, @@ -67,7 +67,7 @@ router.get("/manga/mangareader/filter", async (req, res) => { router.get("/manga/mangareader/chapter/:id", async (req, res) => { try { - const id = req.params.id as unknown as number; + const id = req.params.id as unknown as string; const chapterNumber = req.query.number as unknown as number; const language = req.query .lang as (typeof MangaReaderFilterLanguage)[number]; @@ -88,7 +88,7 @@ router.get("/manga/mangareader/chapter/:id", async (req, res) => { router.get("/manga/mangareader/volume/:id", async (req, res) => { try { - const id = req.params.id as unknown as number; + const id = req.params.id as unknown as string; const chapterNumber = req.query.number as unknown as number; const language = req.query .lang as (typeof MangaReaderFilterLanguage)[number]; diff --git a/src/scraper/sites/manga/MangaReader/MangaReader.ts b/src/scraper/sites/manga/MangaReader/MangaReader.ts index 20ee8963..ab69061a 100644 --- a/src/scraper/sites/manga/MangaReader/MangaReader.ts +++ b/src/scraper/sites/manga/MangaReader/MangaReader.ts @@ -1,7 +1,7 @@ import { Image } from "../../../../types/image"; import { type IMangaResult, - Manga, + MangaMedia, MangaChapter, MangaVolume, } from "../../../../types/manga"; @@ -13,11 +13,12 @@ import { MangaReaderFilterData, } from "./MangaReaderTypes"; import { type IResultSearch, ResultSearch } from "../../../../types/search"; +import { MangaScraperModel } from "../../../../models/MangaScraperModel"; -export class MangaReader { +export class MangaReader extends MangaScraperModel { readonly url = "https://mangareader.to"; - private async GetMangaVolumeRange(mangaId: number) { + private async GetMangaVolumeRange(mangaId: string) { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const $ = load(data); @@ -38,7 +39,7 @@ export class MangaReader { } private async GetSpecificMangaChapterName( - mangaId: number, + mangaId: string, chapterNumber: number, language: (typeof MangaReaderFilterLanguage)[number], type: MangaReaderChapterType @@ -105,7 +106,7 @@ export class MangaReader { return pages; } - async GetMangaInfo(mangaId: number): Promise { + async GetItemInfo(mangaId: string): Promise { try { const { data } = await axios.get(`${this.url}/a-${mangaId}`); const { data: charactersAjaxList } = await axios.get( @@ -139,13 +140,13 @@ export class MangaReader { .map((_, element) => $(element).text().trim()) .get(); - const manga = new Manga(); + const manga = new MangaMedia(); manga.id = mangaId.toString(); - manga.title = title; - manga.altTitles = altTitle; + manga.name = title; + manga.alt_names = altTitle; manga.thumbnail = new Image(thumbnailUrl); - manga.description = description || null; + manga.synopsis = description || null; if (status === "Finished") manga.status = "completed"; else manga.status = "ongoing"; @@ -183,7 +184,7 @@ export class MangaReader { .at(1) .replace(":", ""); - mangaChapter.title = mangaTitle; + mangaChapter.name = mangaTitle; mangaChapter.id = mangaId.toString(); mangaChapter.url = `/manga/mangareader/chapter/${mangaId.toString()}?number=${mangaChapterNumber}&lang=${langCode}`; mangaChapter.images = null; @@ -224,9 +225,9 @@ export class MangaReader { mangaVolume.range = [mangaVolumeRange.at(-1), mangaVolumeRange.at(0)]; mangaVolume.id = mangaId.toString(); - mangaVolume.title = mangaVolumeTitle; - mangaVolume.number = Number(mangaVolumeNumber); - mangaVolume.thumbnail = mangaVolumeThumbnail; + mangaVolume.name = mangaVolumeTitle; + mangaVolume.num = Number(mangaVolumeNumber); + mangaVolume.thumbnail = new Image(mangaVolumeThumbnail); mangaVolume.url = `/manga/mangareader/volume/${mangaId.toString()}?number=${mangaVolumeNumber}&lang=${langVolumeCode}`; manga.volumes.push(mangaVolume); @@ -236,8 +237,8 @@ export class MangaReader { mangaGenres.some((genre) => genre === "Hentai" || genre === "Ecchi") === true ) - manga.isNSFW = true; - else manga.isNSFW = false; + manga.nsfw = true; + else manga.nsfw = false; return manga; } catch (error) { @@ -248,7 +249,7 @@ export class MangaReader { } } - async Filter( + async GetItemByFilter( options: MangaReaderFilterData ): Promise> { const { @@ -324,7 +325,7 @@ export class MangaReader { mangaFilterResults.results.push({ id: mangaResultsID, - title: mangaResultsTitle, + name: mangaResultsTitle, thumbnail: new Image(mangaResultsThumbnail), url: `/manga/mangareader/title/${mangaResultsID}`, }); @@ -334,7 +335,7 @@ export class MangaReader { } async GetMangaChapters( - mangaId: number, + mangaId: string, chapterNumber: number, language: (typeof MangaReaderFilterLanguage)[number], type: MangaReaderChapterType @@ -360,10 +361,10 @@ export class MangaReader { const mangaChapter = new MangaChapter(); const chapterPages = await this.GetMangaPages(chapterId, "chapter"); - mangaChapter.title = mangaChapterName; + mangaChapter.name = mangaChapterName; mangaChapter.id = mangaId; mangaChapter.images = chapterPages; - mangaChapter.number = chapterNumber; + mangaChapter.num = chapterNumber; mangaChapter.url = `/manga/mangareader/chapter/${mangaId.toString()}?number=${chapterNumber}&lang=${language}`; return mangaChapter; @@ -372,11 +373,11 @@ export class MangaReader { const mangaVolumeRange = await this.GetMangaVolumeRange(mangaId); const volumePages = await this.GetMangaPages(chapterId, "volume"); - mangaVolume.title = mangaChapterName; + mangaVolume.name = mangaChapterName; mangaVolume.id = mangaId; mangaVolume.range = [mangaVolumeRange.at(-1), mangaVolumeRange.at(0)]; mangaVolume.images = volumePages; - mangaVolume.number = chapterNumber; + mangaVolume.num = chapterNumber; mangaVolume.url = `/manga/mangareader/volume/${mangaId.toString()}?number=${chapterNumber}&lang=${language}`; return mangaVolume; diff --git a/src/test/MangaReader.spec.ts b/src/test/MangaReader.spec.ts index ca430d44..d5f7ca9f 100644 --- a/src/test/MangaReader.spec.ts +++ b/src/test/MangaReader.spec.ts @@ -18,7 +18,7 @@ describe("MangaReader", () => { it("should return manga info successfully", async () => { const testsList: Array<{ - id: number; + id: string; mangaName: string; altName: string[]; mangaGenres: string[]; @@ -28,7 +28,7 @@ describe("MangaReader", () => { hasChapters: boolean; }> = [ { - id: 65961, + id: "65961", mangaName: "Zashisu", altName: ["ザシス"], mangaGenres: ["Horror", "Mystery", "Psychological", "School", "Seinen"], @@ -38,7 +38,7 @@ describe("MangaReader", () => { hasChapters: true, }, { - id: 65941, + id: "65941", mangaName: "Mitsuba no Monogatari", altName: ["みつばものがたり 呪いの少女と死の輪舞《ロンド》"], mangaGenres: ["Fantasy"], @@ -48,7 +48,7 @@ describe("MangaReader", () => { hasChapters: true, }, { - id: 65795, + id: "65795", mangaName: "Akuyaku Reijou ni Tensei suru no Mahou ni Muchuu de Itara Ouji ni Dekiaisaremashita", altName: ["悪役令嬢に転生するも魔法に夢中でいたら王子に溺愛されました"], @@ -59,7 +59,7 @@ describe("MangaReader", () => { hasChapters: true, }, { - id: 65879, + id: "65879", mangaName: "My Star Is the Lewdest", altName: ["俺の女優が一番淫ら"], mangaGenres: ["Comedy", "Ecchi"], @@ -69,7 +69,7 @@ describe("MangaReader", () => { hasChapters: true, }, { - id: 65789, + id: "65789", mangaName: "Hoop Days", altName: ["ディアボーイズ"], mangaGenres: ["Drama", "Slice of Life", "Sports"], @@ -91,11 +91,11 @@ describe("MangaReader", () => { hasVolumes, hasChapters, } = fields; - const mangaInfo = await mangareader.GetMangaInfo(id); + const mangaInfo = await mangareader.GetItemInfo(id); - expect(mangaInfo.title).toStrictEqual(mangaName); - expect(mangaInfo.altTitles).toStrictEqual(altName); - expect(mangaInfo.isNSFW).toStrictEqual(isNsfw); + expect(mangaInfo.name).toStrictEqual(mangaName); + expect(mangaInfo.alt_names).toStrictEqual(altName); + expect(mangaInfo.nsfw).toStrictEqual(isNsfw); expect(mangaInfo.genres).toStrictEqual(mangaGenres); expect(mangaInfo.status).toStrictEqual(status); @@ -200,7 +200,7 @@ describe("MangaReader", () => { numPage, } = fields; - const filter = await mangareader.Filter({ + const filter = await mangareader.GetItemByFilter({ type: type, status: status, ratingType: ratingType, @@ -225,21 +225,21 @@ describe("MangaReader", () => { it("should return manga chapter pages successfully", async () => { const testsList: Array<{ chapterTitle: string; - id: number; + id: string; chapterNumber: number; language: (typeof MangaReaderFilterLanguage)[number]; type: MangaReaderChapterType; }> = [ { chapterTitle: "Chapter 3: 第 3 話", - id: 65953, + id: "65953", chapterNumber: 3, language: "ja", type: "chapter", }, { chapterTitle: "VOL 2", - id: 65781, + id: "65781", chapterNumber: 2, language: "en", type: "volume", @@ -256,9 +256,9 @@ describe("MangaReader", () => { ); expect(mangaChapters?.images.length).toBeGreaterThanOrEqual(1); - expect(mangaChapters?.title).toStrictEqual(chapterTitle); + expect(mangaChapters?.name).toStrictEqual(chapterTitle); expect(mangaChapters?.id).toStrictEqual(id); - expect(mangaChapters?.number).toStrictEqual(chapterNumber); + expect(mangaChapters?.num).toStrictEqual(chapterNumber); }); }, 5000); }); From a62b82dc29cc0c7bfd81eacba252032c26fa55e2 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Sun, 23 Jun 2024 11:31:24 -0400 Subject: [PATCH 40/64] The Monoschinos scraper was completely updated as there were internal changes on their website --- .../sites/anime/monoschinos/Monoschinos.ts | 156 +++++++++++------- 1 file changed, 93 insertions(+), 63 deletions(-) diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index c6dcf7e1..532a72da 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; import * as cheerio from "cheerio"; import { api, utils } from "../../../../types/utils"; import * as types from "../../../../types/."; @@ -21,11 +21,19 @@ const PageInfo = { async function getEpisodeServers(url: string): Promise { let servers: types.EpisodeServer[] = []; const $ = cheerio.load((await axios.get(url)).data); - $('div.playother').children().each((_i, element) => { - servers.push(new types.EpisodeServer($(element).text().trim(), - Buffer.from($(element).attr('data-player'), 'base64').toString('binary')) - ); + const referenceElement = $('p.fs-5.text-light.my-4'); + const divElement = referenceElement.next('div.d-flex'); + + if (divElement.length === 0) { + throw new Error('The div following the reference element was not found'); + } + + divElement.find('a').each((_i, element) => { + servers.push(new types.EpisodeServer($(element).text().trim(), + $(element).attr('href') + )); }); + return servers; } @@ -35,11 +43,11 @@ async function getEpisodeServers(url: string): Promise { * @param element * @returns */ -async function getEpisodeByElement($, element): Promise { +function getEpisodeByElement($: cheerio.Root, element: cheerio.Element): types.Episode { const episode = new types.Episode(); - episode.number = parseInt($(element).find('div.positioning p').text().trim()); - episode.image = $(element).find('div.animeimgdiv img.animeimghv').attr('data-src'); - episode.name = $(element).find('h2.animetitles').text().trim(); + episode.number = parseInt($(element).find('span.episode').text().trim()); + episode.image = $(element).find('img').attr('data-src'); + episode.name = $(element).find('h2').text(); episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); return episode; } @@ -52,12 +60,9 @@ async function getEpisodeByElement($, element): Promise { async function getLastEpisodes(): Promise { let episodes: types.Episode[] = []; const $ = cheerio.load((await axios.get(PageInfo.url)).data); - const elements = $('div.heroarea div.heroarea1 div.row').children(); - for (let i = 0; i < elements.length; i++) { - if ($(elements[i]).children().length != 0) { - episodes.push(await getEpisodeByElement($, elements[i])); - } - } + $('ul.row.row-cols-xl-4.row-cols-lg-4.row-cols-md-3.row-cols-2').find('li').each((_i, element) => { + episodes.push(getEpisodeByElement($, element)); + }); return episodes; } @@ -66,10 +71,10 @@ async function getLastEpisodes(): Promise { * @param $ * @returns */ -function getGenres($): string[] { +function getGenres($: cheerio.Root): string[] { let genres: string[] = []; - $('div.chapterdetls2 table tbody a').each((_i, element) => { - genres.push($(element).text().trim()) + $('div.tab-content div.tab-pane div.lh-lg a').each((_i, element) => { + genres.push($(element).find('span').text().trim()) }); return genres; } @@ -77,18 +82,33 @@ function getGenres($): string[] { /** * * @param $ + * @param pageData + * @param animeName * @returns */ -function getAnimeEpisodes($): types.Episode[] { +async function getAnimeEpisodes($: cheerio.Root, animeName: string, pageData: AxiosResponse, animePath: string): Promise { + const response = await fetch($('.caplist').attr('data-ajax'), { + "headers": { + "accept": "application/json, text/javascript, */*; q=0.01", + "content-type": "application/x-www-form-urlencoded; charset=UTF-8", + "cookie": pageData.headers['set-cookie'].join(";"), + }, + "body": `_token=${$('meta[name="csrf-token"]').attr('content')}`, + "method": "POST" + }) + let episodes: types.Episode[] = []; - $('div.heromain2 div.allanimes div.row').children().each((_i, element) => { + + const length = (await response.json()).eps.length + const image = $('div img.lazy.w-100').attr('data-src') + for (let i = 1; i <= length; i++) { const episode = new types.Episode(); - episode.number = parseInt($(element).attr('data-episode').trim()); - episode.image = $(element).find('img.animeimghv').attr('data-src'); - episode.name = $(element).find('img.animeimghv').attr('alt'); - episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); + episode.number = i; + episode.image = image; + episode.name = `${animeName} Episodio ${i}`; + episode.url = api.getEpisodeURL(PageInfo, `https://monoschinos2.com/ver/${animePath}-episodio-${i}`); episodes.push(episode); - }); + } return episodes; } @@ -111,8 +131,8 @@ interface ClimaticCalendar { * @param element * @returns the calendar of anime */ -function getAnimeCalendar(element): ClimaticCalendar { - const date = element.find('ol.breadcrumb li.breadcrumb-item').text().trim().split(' '); +function getAnimeCalendar(strDate: string): ClimaticCalendar { + const date = strDate.split(' '); if (date.length != 5) return { year: 0, station: null }; else { @@ -131,52 +151,57 @@ function getAnimeCalendar(element): ClimaticCalendar { */ async function getAnime(url: string): Promise { // The anime page in monoschinos does not define the chronology and type - const $ = cheerio.load((await axios.get(url)).data); - const calendar = getAnimeCalendar($($('div.chapterdetails nav').children()[1])); + const pageData = await axios.get(url); + const $ = cheerio.load(pageData.data); + + const info = $('div.tab-content div.bg-transparent dl').children(); + + const calendar = getAnimeCalendar($(info[3]).text().trim()); const anime = new types.Anime(); - anime.name = $('div.chapterdetails').find('h1').text(); - anime.alt_name = $('div.chapterdetails').find('span.alterno').text(); + anime.name = $(info[5]).text() + anime.alt_name = $(info[7]).text() anime.url = api.getAnimeURL(PageInfo, url); - anime.synopsis = $('div.chapterdetls2 p').text().trim(); + anime.synopsis = $('section.d-sm-none div.mt-3 p').text() + anime.image = new types.Image($('div img.bg-secondary').attr('data-src')) + anime.status = $($('div.tab-content div.col-12.col-md-9 div.ms-2').children()[1]).text(); anime.genres = getGenres($); - anime.image = new types.Image($('div.chapterpic img').attr('src'), $('div.herobg img').attr('src')); - anime.status = 'estreno' === $('div.butns button.btn1').text().toLowerCase().trim(); - anime.episodes = getAnimeEpisodes($); anime.date = new types.Calendar(calendar.year); anime.station = calendar.station; + anime.episodes = await getAnimeEpisodes($, anime.name, pageData, url.split('/').pop()); return anime; } /** + * If url is null then the function will return a list of the last anime + * that were published, otherwise it refers to a url that contains a + * search filtering that among them can be search by name or search by + * category, genre and date * - * @throws {Error} - * @param url - * @returns + * @param url web address with results filtering + * @returns anime list */ async function getLastAnimes(url?: string): Promise { let animes: types.Anime[] = []; - const $ = cheerio.load((await axios.get(url ?? `${PageInfo.url}/emision`)).data); - const elements = $('div.heroarea div.heromain div.row').children(); - for (let i = 0; i < elements.length; i++) { - const href = $(elements[i]).find('a').attr('href'); - if (utils.isUsableValue(href) && href !== 'https://monoschinos2.com/emision?p=2') { - //animes.push(await getAnime(href)); - - let anime = new types.Anime(); - anime.url = $(elements[i]).find('a').attr('href'); - anime.image = new types.Image($(elements[i]).find('a img').attr('src')); - anime.name = $(elements[i]).find('h3.seristitles').text(); - - animes.push(anime); - } + const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); + + const addElement = (element: cheerio.Element) => { + let anime = new types.Anime(); + anime.url = api.getAnimeURL(PageInfo, $(element).find('a').attr('href')) + anime.image = new types.Image($(element).find('img').attr('data-src')); + anime.name = $(element).find('h3').text().trim(); + animes.push(anime); + } + + if (url === null) { + $('ul.row.row-cols-2.row-cols-sm-3').find('li') + .each((_i, element) => addElement(element)); + } else { + $('ul.row').find('li') + .each((_i, element) => addElement(element)); } return animes; } -//console.log(await getLastAnimes('https://monoschinos2.com/animes?categoria=anime&genero=accion&fecha=2023&letra=A')); - -//console.log(await getLastAnimes()) - /** * * @@ -184,15 +209,15 @@ async function getLastAnimes(url?: string): Promise { */ export class Monoschinos { - getLastEpisodes = getLastEpisodes; - getLastAnimes = getLastEpisodes; + getLastEpisodes = getLastEpisodes; + getLastAnimes = (() => getLastAnimes(null)); getEpisodeServers = getEpisodeServers; - getAnime = getAnime; + getAnime = getAnime; - async filter(name: (string | null), category?: string, genre?: string, year?: string, letter?: string): Promise> { + async filter(name: (string | null), category?: string, genre?: string, year?: string): Promise> { const animes = new ResultSearch(); const link = utils.isUsableValue(name) ? `${PageInfo.url}/buscar?q=${name}` : - `${PageInfo.url}/animes?categoria=${category ?? false}&genero=${genre ?? false}&fecha=${year ?? false}&letra=${letter ?? false}`; + `${PageInfo.url}/animes?categoria=${category ?? false}&genero=${genre ?? false}&fecha=${year ?? false}`; (await getLastAnimes(link)) .forEach(element => { if (utils.isUsableValue(element)) { @@ -205,5 +230,10 @@ export class Monoschinos } }; - -//console.log(await getAnime("https://monoschinos2.com/anime/world-dai-star-sub-espanol")); \ No newline at end of file +/****************************** Test API ******************************/ +new Monoschinos().filter(null, null, null, '2022').then(data => { + console.log(data) +}).catch(error => console.log(error)) +/*getAnime("https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol").then(data => { + console.log(data) +}).catch(error => console.log(error))*/ \ No newline at end of file From 21ff836599dc7cfc54c0abe3174c214d75cb68d0 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Sun, 23 Jun 2024 12:20:29 -0400 Subject: [PATCH 41/64] The code was migrated to the new implementation of the API classes --- .../sites/anime/monoschinos/Monoschinos.ts | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index 532a72da..0eddda8a 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -2,7 +2,7 @@ import axios, { AxiosResponse } from "axios"; import * as cheerio from "cheerio"; import { api, utils } from "../../../../types/utils"; import * as types from "../../../../types/."; -import { ResultSearch, IResultSearch, IAnimeSearch } from "../../../../types/search"; +import { ResultSearch, IResultSearch, AnimeResult } from "../../../../types/search"; const PageInfo = { name: 'monoschinos', @@ -44,11 +44,11 @@ async function getEpisodeServers(url: string): Promise { * @returns */ function getEpisodeByElement($: cheerio.Root, element: cheerio.Element): types.Episode { - const episode = new types.Episode(); - episode.number = parseInt($(element).find('span.episode').text().trim()); - episode.image = $(element).find('img').attr('data-src'); - episode.name = $(element).find('h2').text(); - episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); + const episode = new types.Episode(); + episode.num = parseInt($(element).find('span.episode').text().trim()); + episode.thumbnail = new types.Image($(element).find('img').attr('data-src')); + episode.name = $(element).find('h2').text(); + episode.url = api.getEpisodeURL(PageInfo, $(element).find('a').attr('href')); return episode; } @@ -102,11 +102,11 @@ async function getAnimeEpisodes($: cheerio.Root, animeName: string, pageData: Ax const length = (await response.json()).eps.length const image = $('div img.lazy.w-100').attr('data-src') for (let i = 1; i <= length; i++) { - const episode = new types.Episode(); - episode.number = i; - episode.image = image; - episode.name = `${animeName} Episodio ${i}`; - episode.url = api.getEpisodeURL(PageInfo, `https://monoschinos2.com/ver/${animePath}-episodio-${i}`); + const episode = new types.Episode(); + episode.num = i; + episode.thumbnail = new types.Image(image); + episode.name = `${animeName} Episodio ${i}`; + episode.url = api.getEpisodeURL(PageInfo, `https://monoschinos2.com/ver/${animePath}-episodio-${i}`); episodes.push(episode); } return episodes; @@ -149,25 +149,25 @@ function getAnimeCalendar(strDate: string): ClimaticCalendar { * @param url * @returns */ -async function getAnime(url: string): Promise { +async function getAnime(url: string): Promise { // The anime page in monoschinos does not define the chronology and type - const pageData = await axios.get(url); - const $ = cheerio.load(pageData.data); - - const info = $('div.tab-content div.bg-transparent dl').children(); - - const calendar = getAnimeCalendar($(info[3]).text().trim()); - const anime = new types.Anime(); - anime.name = $(info[5]).text() - anime.alt_name = $(info[7]).text() - anime.url = api.getAnimeURL(PageInfo, url); - anime.synopsis = $('section.d-sm-none div.mt-3 p').text() - anime.image = new types.Image($('div img.bg-secondary').attr('data-src')) - anime.status = $($('div.tab-content div.col-12.col-md-9 div.ms-2').children()[1]).text(); - anime.genres = getGenres($); - anime.date = new types.Calendar(calendar.year); - anime.station = calendar.station; - anime.episodes = await getAnimeEpisodes($, anime.name, pageData, url.split('/').pop()); + const pageData = await axios.get(url); + const $ = cheerio.load(pageData.data); + + const info = $('div.tab-content div.bg-transparent dl').children(); + + const calendar = getAnimeCalendar($(info[3]).text().trim()); + const anime = new types.AnimeMedia(); + anime.name = $(info[5]).text() + anime.alt_names = $(info[7]).text() + anime.url = api.getAnimeURL(PageInfo, url); + anime.synopsis = $('section.d-sm-none div.mt-3 p').text() + anime.image = new types.Image($('div img.bg-secondary').attr('data-src')) + anime.status = $($('div.tab-content div.col-12.col-md-9 div.ms-2').children()[1]).text(); + anime.genres = getGenres($); + anime.date = new types.Calendar(calendar.year); + anime.station = calendar.station; + anime.episodes = await getAnimeEpisodes($, anime.name, pageData, url.split('/').pop()); return anime; } @@ -180,12 +180,12 @@ async function getAnime(url: string): Promise { * @param url web address with results filtering * @returns anime list */ -async function getLastAnimes(url?: string): Promise { - let animes: types.Anime[] = []; +async function getLastAnimes(url?: string): Promise { + let animes: types.AnimeMedia[] = []; const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); const addElement = (element: cheerio.Element) => { - let anime = new types.Anime(); + let anime = new types.AnimeMedia(); anime.url = api.getAnimeURL(PageInfo, $(element).find('a').attr('href')) anime.image = new types.Image($(element).find('img').attr('data-src')); anime.name = $(element).find('h3').text().trim(); @@ -214,8 +214,8 @@ export class Monoschinos getEpisodeServers = getEpisodeServers; getAnime = getAnime; - async filter(name: (string | null), category?: string, genre?: string, year?: string): Promise> { - const animes = new ResultSearch(); + async filter(name: (string | null), category?: string, genre?: string, year?: string): Promise> { + const animes = new ResultSearch(); const link = utils.isUsableValue(name) ? `${PageInfo.url}/buscar?q=${name}` : `${PageInfo.url}/animes?categoria=${category ?? false}&genero=${genre ?? false}&fecha=${year ?? false}`; (await getLastAnimes(link)) From 3da604b97199bf78c140decb07e943532c59b4b7 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Sun, 23 Jun 2024 12:26:06 -0400 Subject: [PATCH 42/64] The "letter" field was removed because Monoschinos no longer implements this function in its search filters --- src/routes/v1/anime/monoschinos/MonosChinosRoute.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts index 23748f40..816f4e29 100644 --- a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts +++ b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts @@ -40,10 +40,9 @@ r.get("/anime/monoschinos/filter", async (req, res) => { const cat = req.query.category as string; const gen = req.query.gen as string; const year = req.query.year as string; - const letter = req.query.letter as string; const monos = new Monoschinos(); - const animeInfo = await monos.filter(title, cat, gen, year, letter); + const animeInfo = await monos.filter(title, cat, gen, year); res.send(animeInfo); } catch (error) { console.log(error); From 0413f2c2a0c633b21bf83077c59b82c2a3984e0d Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Sun, 23 Jun 2024 12:41:55 -0400 Subject: [PATCH 43/64] The code was migrated to the new implementation of the classes in the API --- src/scraper/sites/anime/tioanime/TioAnime.ts | 21 ++++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/scraper/sites/anime/tioanime/TioAnime.ts b/src/scraper/sites/anime/tioanime/TioAnime.ts index dd0e14ba..58131811 100644 --- a/src/scraper/sites/anime/tioanime/TioAnime.ts +++ b/src/scraper/sites/anime/tioanime/TioAnime.ts @@ -5,7 +5,7 @@ import * as types from "../../../../types/."; import { ResultSearch, type IResultSearch, - type IAnimeSearch, + type AnimeResult, } from "../../../../types/search"; const PageInfo = { @@ -72,9 +72,9 @@ async function getAnimeEpisodes(data) { data.episodes.forEach((episode_number) => { let episode = new types.Episode(); episode.name = `${data.info[2]} Capitulo ${episode_number}`; - episode.image = PageInfo.url + `/uploads/thumbs/${data.info[0]}.jpg`; + episode.thumbnail = new types.Image(PageInfo.url + `/uploads/thumbs/${data.info[0]}.jpg`); episode.url = `/anime/tioanime/episode/${data.info[1]}-${episode_number}`; - episode.number = episode_number; + episode.num = episode_number; __episodes.push(episode); }); return __episodes; @@ -83,8 +83,7 @@ async function getAnimeEpisodes(data) { function getEpisode($, element) { const title = $(element).find("h3.title").text().trim(); const episode = new types.Episode(); - episode.image = - PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src"); + episode.thumbnail = new types.Image(PageInfo.url + $(element).find("figure.fa-play-circle img").attr("src")) episode.url = $(element) .find("article.episode a") .attr("href") @@ -93,7 +92,7 @@ function getEpisode($, element) { for (let i = title.length - 1; i >= 0; i--) { if (title[i] == " ") { episode.name = title.substring(0, i).trim(); - episode.number = parseInt(title.substring(i + 1, title.length)); + episode.num = parseInt(title.substring(i + 1, title.length)); break; } } @@ -144,7 +143,7 @@ async function getAnime(url) { // It is possible that the object returned by the getScriptAnimeInfo function is null. if (data == null) throw new Error("The getScriptAnimeInfo() function returns a null value."); - const anime = new types.Anime(); + const anime = new types.AnimeMedia(); anime.name = $("div.container h1.title").text(); //anime.url = url; anime.url = url.replace( @@ -192,7 +191,7 @@ async function getAnime(url) { async function getLastAnimes(url: string) { console.log(url); try { - let animes: types.IAnime[] = []; + let animes: types.AnimeMedia[] = []; const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); const elements = $( utils.isUsableValue(url) @@ -213,7 +212,7 @@ async function getLastAnimes(url: string) { } async function getSectionContents(section: number) { - let animes: types.IAnime[] = []; + let animes: types.AnimeMedia[] = []; try { const $ = cheerio.load( (await axios.get(`${PageInfo.url}/directorio?type%5B%5D=${section}`)).data @@ -284,8 +283,8 @@ export class TioAnime { year_range?: IYearRange, status?: number, sort?: string - ): Promise> { - const animes = new ResultSearch(); + ): Promise> { + const animes = new ResultSearch(); let usable; if (!(usable = utils.isUsableValue(name) && name.trim().length != 0)) year_range ?? From 5a1824ddb6c613d0d52222a848db44355ea8795e Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Sun, 23 Jun 2024 12:43:08 -0400 Subject: [PATCH 44/64] Added the "last" option to request the latest anime and episodes published --- .../v1/anime/monoschinos/MonosChinosRoute.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts index 816f4e29..4e9ede99 100644 --- a/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts +++ b/src/routes/v1/anime/monoschinos/MonosChinosRoute.ts @@ -50,4 +50,23 @@ r.get("/anime/monoschinos/filter", async (req, res) => { } }); +//last episodes +r.get("/anime/monoschinos/last/:option", async (req, res) => { + try { + const { option } = req.params; + + const monos = new Monoschinos(); + if ("episodes" === option) { + res.send(await monos.getLastEpisodes()); + } else if ("animes" === option) { + res.send(await monos.getLastAnimes()); + } else { + throw "Invalid option in the URL"; + } + } catch (error) { + console.log(error); + res.status(500).send(error); + } +}); + export default r; From c777c570d67e44c5218396bf6032c36da291ab40 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Mon, 24 Jun 2024 08:33:37 -0400 Subject: [PATCH 45/64] Removed an unnecessary call to console.log() --- src/scraper/sites/anime/tioanime/TioAnime.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/tioanime/TioAnime.ts b/src/scraper/sites/anime/tioanime/TioAnime.ts index 58131811..7961b980 100644 --- a/src/scraper/sites/anime/tioanime/TioAnime.ts +++ b/src/scraper/sites/anime/tioanime/TioAnime.ts @@ -188,8 +188,7 @@ async function getAnime(url) { return anime; } -async function getLastAnimes(url: string) { - console.log(url); +async function getLastAnimes(url: string | null) { try { let animes: types.AnimeMedia[] = []; const $ = cheerio.load((await axios.get(url ?? PageInfo.url)).data); From e5b318e0b2a89321afb05309d07f295f45becdb8 Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Mon, 24 Jun 2024 08:34:30 -0400 Subject: [PATCH 46/64] More checks have been added to the TioAnime test --- src/test/TioAnime.spec.ts | 65 +++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/src/test/TioAnime.spec.ts b/src/test/TioAnime.spec.ts index 0b96b993..8363269b 100644 --- a/src/test/TioAnime.spec.ts +++ b/src/test/TioAnime.spec.ts @@ -12,27 +12,52 @@ describe("TioAnime", () => { ); expect(animeInfo.name).toBe("Date A Live"); - expect(animeInfo.image.url).toContain(".jpg"); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); + expect(animeInfo.image.url).toMatch(/\.jpg$/); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - expect(animeInfo.episodes.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); + expect((await tioanime.getLastAnimes(null)).length).toBeGreaterThan(0); + expect((await tioanime.getLastEpisodes()).length).toBeGreaterThan(0); + expect((await tioanime.getLastMovies()).length).toBeGreaterThan(0); + expect((await tioanime.getLastOnas()).length).toBeGreaterThan(0); + expect((await tioanime.getLastOvas()).length).toBeGreaterThan(0); + expect((await tioanime.getEpisodeServers('https://tioanime.com/ver/date-a-live-1')).length).toBeGreaterThan(0); }); +}); - /*it('should get episode servers successfully', async () => { - const episodeServers = await tioanime.getEpisodeServers('https://tioanime.com/ver/isekai-nonbiri-nouka-9'); - expect(episodeServers.length).toBeGreaterThan(0); - for (let i = 0; i < episodeServers.length; i++) { - const server = episodeServers[i]; - expect(server.name.length).toBeGreaterThan(0); - if (server.file_url != undefined && server.file_url != null) { - expect(server.file_url.length).toBeGreaterThan(0); - } - } - }); - it('should filter anime successfully', async () => { - const result = await tioanime.filter("", ["1"], ["accion"], { begin: 1950, end: 2023 }, 2, "recent"); - expect(result.results.length).toBeGreaterThan(0); - });*/ -}); +/*async function test() { + let tioanime: TioAnime; + tioanime = new TioAnime(); + const animeInfo = await tioanime.getAnime( + "https://tioanime.com/anime/date-a-live", + ); + + console.log('> Name: ') + console.log(animeInfo.name); + console.log('> Image url: ') + console.log(animeInfo.image.url); + console.log('> Synopsis length: ') + console.log(animeInfo.synopsis?.length); + console.log('> Chronology length: ') + console.log(animeInfo.chronology?.length); + console.log('> Genres length: ') + console.log(animeInfo.genres?.length); + console.log('> Episodes length: ') + console.log(animeInfo.episodes?.length); + console.log('> function getLastAnimes(): ') + console.log((await tioanime.getLastAnimes(null)).length); + console.log('> function getLastEpisodes(): ') + console.log((await tioanime.getLastEpisodes()).length); + console.log('> function getLastMovies(): ') + console.log((await tioanime.getLastMovies()).length); + console.log('> function getLastOnas(): ') + console.log((await tioanime.getLastOnas()).length); + console.log('> function getLastOvas(): ') + console.log((await tioanime.getLastOvas()).length); + console.log('> function getEpisodeServers(): ') + console.log((await tioanime.getEpisodeServers('https://tioanime.com/ver/isekai-nonbiri-nouka-9')).length); +} + +test();*/ \ No newline at end of file From ca92fb3368de55cd2e7bd37db99ce0c322959e7c Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Mon, 24 Jun 2024 08:35:07 -0400 Subject: [PATCH 47/64] Monoschinos test has been added! --- src/test/Monoschinos.spec.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/Monoschinos.spec.ts diff --git a/src/test/Monoschinos.spec.ts b/src/test/Monoschinos.spec.ts new file mode 100644 index 00000000..6790d8a5 --- /dev/null +++ b/src/test/Monoschinos.spec.ts @@ -0,0 +1,24 @@ +import { Monoschinos } from "../scraper/sites/anime/monoschinos/Monoschinos"; + +describe("Monoschinos", () => { + let monos: Monoschinos; + + beforeEach(() => { + monos = new Monoschinos(); + }); + it("should get anime info successfully", async () => { + const animeInfo = await monos.getAnime( + "https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol", + ); + + expect(animeInfo.name).toBe("Date A Live"); + expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); + expect(animeInfo.chronology?.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); + expect((await monos.getLastAnimes()).length).toBeGreaterThan(0); + expect((await monos.getLastEpisodes()).length).toBeGreaterThan(0); + //expect((await monos.getEpisodeServers('https://tioanime.com/ver/isekai-nonbiri-nouka-9')).length).toBeGreaterThan(0); + }); +}); \ No newline at end of file From 68df70374427ddb615a2c3631fe0471f45710d1f Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Mon, 24 Jun 2024 08:44:00 -0400 Subject: [PATCH 48/64] A filter test was removed --- src/scraper/sites/anime/monoschinos/Monoschinos.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scraper/sites/anime/monoschinos/Monoschinos.ts b/src/scraper/sites/anime/monoschinos/Monoschinos.ts index 0eddda8a..0cf5686a 100644 --- a/src/scraper/sites/anime/monoschinos/Monoschinos.ts +++ b/src/scraper/sites/anime/monoschinos/Monoschinos.ts @@ -231,9 +231,9 @@ export class Monoschinos }; /****************************** Test API ******************************/ -new Monoschinos().filter(null, null, null, '2022').then(data => { +/*new Monoschinos().filter(null, null, null, '2022').then(data => { console.log(data) -}).catch(error => console.log(error)) +}).catch(error => console.log(error))*/ /*getAnime("https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol").then(data => { console.log(data) }).catch(error => console.log(error))*/ \ No newline at end of file From a09c5846d873528d99274907b02f6a5ec30d38ec Mon Sep 17 00:00:00 2001 From: Zukaritasu Date: Mon, 24 Jun 2024 08:50:31 -0400 Subject: [PATCH 49/64] Some changes were included in the test and the Chronology test was eliminated --- src/test/Monoschinos.spec.ts | 47 ++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/src/test/Monoschinos.spec.ts b/src/test/Monoschinos.spec.ts index 6790d8a5..2e772abe 100644 --- a/src/test/Monoschinos.spec.ts +++ b/src/test/Monoschinos.spec.ts @@ -1,6 +1,6 @@ import { Monoschinos } from "../scraper/sites/anime/monoschinos/Monoschinos"; -describe("Monoschinos", () => { +describe('Monoschinos', () => { let monos: Monoschinos; beforeEach(() => { @@ -8,17 +8,50 @@ describe("Monoschinos", () => { }); it("should get anime info successfully", async () => { const animeInfo = await monos.getAnime( - "https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol", + 'https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol', ); - expect(animeInfo.name).toBe("Date A Live"); - expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.name).toBe('Date A Live'); + expect(animeInfo.image.url).toContain('.jpg'); expect(animeInfo.synopsis?.length).toBeGreaterThan(0); - expect(animeInfo.chronology?.length).toBeGreaterThan(0); + // The chronology function does not exist in Monoschinos + //expect(animeInfo.chronology?.length).toBeGreaterThan(0); expect(animeInfo.genres?.length).toBeGreaterThan(0); expect(animeInfo.episodes?.length).toBeGreaterThan(0); expect((await monos.getLastAnimes()).length).toBeGreaterThan(0); expect((await monos.getLastEpisodes()).length).toBeGreaterThan(0); - //expect((await monos.getEpisodeServers('https://tioanime.com/ver/isekai-nonbiri-nouka-9')).length).toBeGreaterThan(0); + expect((await monos.getEpisodeServers('https://monoschinos2.com/ver/one-room-hiatari-futsuu-tenshi-tsuki-episodio-1')).length).toBeGreaterThan(0); }); -}); \ No newline at end of file +}); + + +/*async function test() { + let monos: Monoschinos; + monos = new Monoschinos(); + + const animeInfo = await monos.getAnime( + "https://monoschinos2.com/anime/date-a-live-i-sub-espanol", + ); + + console.log("> Name: ") + console.log(animeInfo.name) + console.log("> Image url: ") + console.log(animeInfo.image.url) + console.log("> Synopsis length: ") + console.log(animeInfo.synopsis?.length) + //console.log("> Chronology length: ") + //console.log(animeInfo.chronology?.length) + console.log("> Genres length: ") + console.log(animeInfo.genres?.length) + console.log("> Episodes count: ") + console.log(animeInfo.episodes?.length) + + console.log("> function getLastAnimes(): ") + console.log((await monos.getLastAnimes()).length) + console.log("> function getLastAnimes(): ") + console.log((await monos.getLastEpisodes()).length) + console.log("> function getEpisodeServers(): ") + console.log((await monos.getEpisodeServers('https://monoschinos2.com/ver/date-a-live-i-episodio-1')).length) +} + +test();*/ \ No newline at end of file From 578332236d60de8678b126fe74da893f31b8303b Mon Sep 17 00:00:00 2001 From: Zukaritasu <77790801+Zukaritasu@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:19:55 -0400 Subject: [PATCH 50/64] Update Monoschinos.spec.ts In the Monoschinos test, the validation field of the name 'Date A Live' was replaced by 'One Room, Hiatari Futsuu, Tenshi-tsuki' --- src/test/Monoschinos.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/Monoschinos.spec.ts b/src/test/Monoschinos.spec.ts index 2e772abe..70ae8274 100644 --- a/src/test/Monoschinos.spec.ts +++ b/src/test/Monoschinos.spec.ts @@ -11,7 +11,7 @@ describe('Monoschinos', () => { 'https://monoschinos2.com/anime/one-room-hiatari-futsuu-tenshi-tsuki-sub-espanol', ); - expect(animeInfo.name).toBe('Date A Live'); + expect(animeInfo.name).toBe('One Room, Hiatari Futsuu, Tenshi-tsuki.'); expect(animeInfo.image.url).toContain('.jpg'); expect(animeInfo.synopsis?.length).toBeGreaterThan(0); // The chronology function does not exist in Monoschinos @@ -54,4 +54,4 @@ describe('Monoschinos', () => { console.log((await monos.getEpisodeServers('https://monoschinos2.com/ver/date-a-live-i-episodio-1')).length) } -test();*/ \ No newline at end of file +test();*/ From dfdf43d325b28f7dfcb998d6c5f18e81da007ffa Mon Sep 17 00:00:00 2001 From: Zukaritasu <77790801+Zukaritasu@users.noreply.github.com> Date: Mon, 24 Jun 2024 14:22:01 -0400 Subject: [PATCH 51/64] Update TioAnime.spec.ts The test time was increased to 40 seconds because TioAnime takes a long time to respond --- src/test/TioAnime.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/TioAnime.spec.ts b/src/test/TioAnime.spec.ts index 8363269b..74ddaa2e 100644 --- a/src/test/TioAnime.spec.ts +++ b/src/test/TioAnime.spec.ts @@ -23,7 +23,7 @@ describe("TioAnime", () => { expect((await tioanime.getLastOnas()).length).toBeGreaterThan(0); expect((await tioanime.getLastOvas()).length).toBeGreaterThan(0); expect((await tioanime.getEpisodeServers('https://tioanime.com/ver/date-a-live-1')).length).toBeGreaterThan(0); - }); + }, 40000 /*ms*/); }); @@ -60,4 +60,4 @@ describe("TioAnime", () => { console.log((await tioanime.getEpisodeServers('https://tioanime.com/ver/isekai-nonbiri-nouka-9')).length); } -test();*/ \ No newline at end of file +test();*/ From 2d9a4450fb6df435741bccaf6d9052767601f3f0 Mon Sep 17 00:00:00 2001 From: yako Date: Tue, 9 Jul 2024 22:27:07 -0600 Subject: [PATCH 52/64] Add animeflv with new models --- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index ab0330b2..38b2bca2 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { load } from "cheerio"; -import { Anime, Chronology } from "../../../../types/anime"; +import { AnimeMedia, Chronology } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { Genres, @@ -9,17 +9,17 @@ import { TypeAnimeflv, } from "./animeflv_helper"; import { - AnimeSearch, ResultSearch, type IResultSearch, - type IAnimeSearch, + type IAnimeResult, + AnimeResult, } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; export class AnimeFlv extends AnimeScraperModel { readonly url = "https://animeflv.ws"; - async GetItemInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = load(data); @@ -29,9 +29,9 @@ export class AnimeFlv extends AnimeScraperModel { const status = $("p.AnmStts span").text().trim(); const synopsis = $("div.Description").text().trim(); const episodes = $(".ListCaps li a"); - const AnimeReturn = new Anime(); + const AnimeReturn = new AnimeMedia(); AnimeReturn.name = title; - AnimeReturn.alt_name = [...title_alt.split(",")]; + AnimeReturn.alt_names = [...title_alt.split(",")]; AnimeReturn.image = { url: img, }; @@ -60,8 +60,11 @@ export class AnimeFlv extends AnimeScraperModel { "/anime", "/anime/flv" )}`; - episode.number = $(e).children("p").last().text().trim(); - episode.image = $(e).children("figure").find(".lazy").attr("src"); + episode.num = Number($(e).children("p").last().text().trim()); + episode.thumbnail.url = $(e) + .children("figure") + .find(".lazy") + .attr("src"); AnimeReturn.episodes.push(episode); }); return AnimeReturn; @@ -84,7 +87,7 @@ export class AnimeFlv extends AnimeScraperModel { ord?: OrderAnimeflv, page?: number, title?: string - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.url}/browse`, { params: { @@ -99,10 +102,10 @@ export class AnimeFlv extends AnimeScraperModel { }); const $ = load(data); const infoList = $("ul.ListAnimes li"); - const data_filter = new ResultSearch(); + const data_filter = new ResultSearch(); data_filter.results = []; infoList.each((_i, e) => { - const info = new AnimeSearch(); + const info = new AnimeResult(); info.name = $(e).find("h3").text().trim(); info.image = $(e) @@ -133,7 +136,7 @@ export class AnimeFlv extends AnimeScraperModel { const episodeReturn = new Episode(); episodeReturn.name = title; episodeReturn.url = `/anime/flv/episode/${episode}`; - episodeReturn.number = numberEpisode as unknown as string; + episodeReturn.num = Number(numberEpisode); episodeReturn.servers = []; const promises = getLinks.map(async (_i, e) => { From 83df4bc03ad035cddc48fb9783c695e9e9281d89 Mon Sep 17 00:00:00 2001 From: yako Date: Tue, 9 Jul 2024 22:28:20 -0600 Subject: [PATCH 53/64] Add animeflv with news models --- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 27 +++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index ab0330b2..38b2bca2 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -1,6 +1,6 @@ import axios from "axios"; import { load } from "cheerio"; -import { Anime, Chronology } from "../../../../types/anime"; +import { AnimeMedia, Chronology } from "../../../../types/anime"; import { Episode, EpisodeServer } from "../../../../types/episode"; import { Genres, @@ -9,17 +9,17 @@ import { TypeAnimeflv, } from "./animeflv_helper"; import { - AnimeSearch, ResultSearch, type IResultSearch, - type IAnimeSearch, + type IAnimeResult, + AnimeResult, } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; export class AnimeFlv extends AnimeScraperModel { readonly url = "https://animeflv.ws"; - async GetItemInfo(anime: string): Promise { + async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = load(data); @@ -29,9 +29,9 @@ export class AnimeFlv extends AnimeScraperModel { const status = $("p.AnmStts span").text().trim(); const synopsis = $("div.Description").text().trim(); const episodes = $(".ListCaps li a"); - const AnimeReturn = new Anime(); + const AnimeReturn = new AnimeMedia(); AnimeReturn.name = title; - AnimeReturn.alt_name = [...title_alt.split(",")]; + AnimeReturn.alt_names = [...title_alt.split(",")]; AnimeReturn.image = { url: img, }; @@ -60,8 +60,11 @@ export class AnimeFlv extends AnimeScraperModel { "/anime", "/anime/flv" )}`; - episode.number = $(e).children("p").last().text().trim(); - episode.image = $(e).children("figure").find(".lazy").attr("src"); + episode.num = Number($(e).children("p").last().text().trim()); + episode.thumbnail.url = $(e) + .children("figure") + .find(".lazy") + .attr("src"); AnimeReturn.episodes.push(episode); }); return AnimeReturn; @@ -84,7 +87,7 @@ export class AnimeFlv extends AnimeScraperModel { ord?: OrderAnimeflv, page?: number, title?: string - ): Promise> { + ): Promise> { try { const { data } = await axios.get(`${this.url}/browse`, { params: { @@ -99,10 +102,10 @@ export class AnimeFlv extends AnimeScraperModel { }); const $ = load(data); const infoList = $("ul.ListAnimes li"); - const data_filter = new ResultSearch(); + const data_filter = new ResultSearch(); data_filter.results = []; infoList.each((_i, e) => { - const info = new AnimeSearch(); + const info = new AnimeResult(); info.name = $(e).find("h3").text().trim(); info.image = $(e) @@ -133,7 +136,7 @@ export class AnimeFlv extends AnimeScraperModel { const episodeReturn = new Episode(); episodeReturn.name = title; episodeReturn.url = `/anime/flv/episode/${episode}`; - episodeReturn.number = numberEpisode as unknown as string; + episodeReturn.num = Number(numberEpisode); episodeReturn.servers = []; const promises = getLinks.map(async (_i, e) => { From 4743bce7e1a88f1f0981eed7b402c87d3f3114fb Mon Sep 17 00:00:00 2001 From: yako Date: Tue, 9 Jul 2024 22:55:28 -0600 Subject: [PATCH 54/64] wait for update and delete zoro and nhentai test --- src/index.ts | 6 ------ src/test/Animeflv.spec.ts | 28 +++++++++++++++++++++------- src/test/Nhentai.spec.ts | 36 ------------------------------------ src/test/Zoro.spec.ts | 22 ---------------------- 4 files changed, 21 insertions(+), 71 deletions(-) delete mode 100644 src/test/Nhentai.spec.ts delete mode 100644 src/test/Zoro.spec.ts diff --git a/src/index.ts b/src/index.ts index 7b443354..60d178c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,6 @@ import cors from "cors"; /* Anime */ import flv from "../src/routes/v1/anime/animeflv/AnimeflvRoutes"; import latinhd from "../src/routes/v1/anime/animelatinohd/AnimeLatinoHDRoutes"; -//import gogoanime from "../src/routes/v1/anime/gogoanime/GogoAnimeRoute"; -import zoro from "../src/routes/v1/anime/zoro/ZoroRoutes"; import monoschinos from "../src/routes/v1/anime/monoschinos/MonosChinosRoute"; import tioanime from "../src/routes/v1/anime/tioanime/TioAnimeRoute"; import WcoStream from "../src/routes/v1/anime/wcostream/wcostreamRoutes"; @@ -19,7 +17,6 @@ import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; /* Manga */ import comick from "../src/routes/v1/manga/comick/ComickRoutes"; import inmanga from "../src/routes/v1/manga/inmanga/InmangaRoutes"; -import nhentai from "../src/routes/v1/manga/nhentai/NhentaiRoutes"; import mangareader from "../src/routes/v1/manga/mangareader/MangaReaderRoutes"; import manganelo from "../src/routes/v1/manga/manganelo/ManganeloRoutes"; @@ -40,9 +37,7 @@ app.use(cors()); /*anime*/ app.use(flv); app.use(latinhd); -//app.use(gogoanime); app.use(monoschinos); -app.use(zoro); app.use(tioanime); app.use(WcoStream); app.use(AnimeBlix); @@ -53,7 +48,6 @@ app.use(Animevostfr); /*Manga*/ app.use(comick); app.use(inmanga); -app.use(nhentai); app.use(mangareader); app.use(manganelo); /*Manga*/ diff --git a/src/test/Animeflv.spec.ts b/src/test/Animeflv.spec.ts index adcee4f7..24b09879 100644 --- a/src/test/Animeflv.spec.ts +++ b/src/test/Animeflv.spec.ts @@ -1,8 +1,11 @@ import { AnimeFlv } from "../scraper/sites/anime/animeflv/AnimeFlv"; +import { AnimeMedia } from "../types/anime"; +import { Episode } from "../types/episode"; import { - StatusAnimeflv, Genres, + StatusAnimeflv, } from "../scraper/sites/anime/animeflv/animeflv_helper"; + describe("AnimeFlv", () => { let animeFlv: AnimeFlv; @@ -11,15 +14,16 @@ describe("AnimeFlv", () => { }); it("should get anime info successfully", async () => { - const animeInfo = await animeFlv.GetItemInfo("wonder-egg-priority"); + const animeInfo: AnimeMedia = + await animeFlv.GetItemInfo("25jigen-no-ririsa"); expect(animeInfo.name).toBe("Wonder Egg Priority"); - expect(animeInfo.alt_name).toContain("ワンダーエッグ・プライオリティ"); + expect(animeInfo.alt_names).toContain("ワンダーエッグ・プライオリティ"); expect(animeInfo.image.url).toContain(".jpg"); - expect(animeInfo.status).toBe("En emision"); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); + expect(animeInfo.status).toBe("Finalizado"); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - expect(animeInfo.episodes.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); }); it("should filter anime successfully", async () => { @@ -33,4 +37,14 @@ describe("AnimeFlv", () => { ); expect(result.results.length).toBeGreaterThan(0); }); + + it("should get episode servers successfully", async () => { + const episode: Episode = await animeFlv.GetEpisodeServers( + "wonder-egg-priority-01" + ); + expect(episode.name).toBeTruthy(); + expect(episode.url).toContain("/anime/flv/episode/wonder-egg-priority-01"); + expect(episode.num).toBe(1); + expect(episode?.servers?.length).toBeGreaterThan(0); + }); }); diff --git a/src/test/Nhentai.spec.ts b/src/test/Nhentai.spec.ts deleted file mode 100644 index b405de9d..00000000 --- a/src/test/Nhentai.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Nhentai } from "../../src/scraper/sites/manga/nhentai/Nhentai"; -import { IMangaChapter, IMangaResult, Manga } from "../types/manga"; - -describe("It returns a list of animes related that name filter", () => { - it("it should match that fields", async () => { - const mangasHentai: IMangaResult[] = await new Nhentai().filter( - "Evangelion", - ); - - expect(mangasHentai[0].id).toBe("403447"); - expect(mangasHentai[0].title).toBe( - "[Cassino (Magarikouji Lily)] Playboys (2) – Neon Genesis Evangelion dj [Eng]", - ); - }); -}); - -describe("Manga info tests", () => { - it("it should return a manga information from manga id", async () => { - const getMangaInfo: Manga = await new Nhentai().getMangaInfo("403447"); - - expect(getMangaInfo.title).toEqual( - "[Cassino (Magarikouji Lily)] Playboys (2) – Neon Genesis Evangelion dj [Eng]", - ); - expect(getMangaInfo.id).toEqual("403447"); - }); -}); - -describe("Manga chapters", () => { - it("it should return a manga chapters from manga id", async () => { - const getMangaChapters: IMangaChapter[] = - await new Nhentai().getMangaChapters("403447"); - expect(getMangaChapters[0].images[0]).toEqual( - "https://t7.nhentai.net/galleries/2223278/1t.jpg", - ); - }); -}); diff --git a/src/test/Zoro.spec.ts b/src/test/Zoro.spec.ts deleted file mode 100644 index 5e4d1654..00000000 --- a/src/test/Zoro.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Zoro } from "../scraper/sites/anime/zoro/Zoro"; - -describe("Zoro", () => { - let zoro: Zoro; - - beforeEach(() => { - zoro = new Zoro(); - }); - it("should get anime info successfully", async () => { - const animeInfo = await zoro.GetItemInfo("tokyo-ghoul-790"); - expect(animeInfo.name).toBe("Tokyo Ghoul"); - expect(animeInfo.alt_name).toContain("東京喰種-トーキョーグール-"); - expect(animeInfo.image.url).toContain(".jpg"); - expect(animeInfo.synopsis.length).toBeGreaterThan(0); - expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres.length).toBeGreaterThan(0); - }); - it("should filter anime successfully", async () => { - const result = await zoro.GetItemByFilter("2"); - expect(result.results.length).toBeGreaterThan(0); - }); -}); From 888f097c9271a796d51b5c93eb1af09c7f4bf644 Mon Sep 17 00:00:00 2001 From: yako Date: Fri, 19 Jul 2024 14:18:14 -0600 Subject: [PATCH 55/64] animeflv --- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 114 +++++++++++++------ 1 file changed, 78 insertions(+), 36 deletions(-) diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 38b2bca2..307eeee2 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -1,7 +1,7 @@ import axios from "axios"; import { load } from "cheerio"; import { AnimeMedia, Chronology } from "../../../../types/anime"; -import { Episode, EpisodeServer } from "../../../../types/episode"; +import { Episode } from "../../../../types/episode"; import { Genres, OrderAnimeflv, @@ -17,18 +17,26 @@ import { import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; export class AnimeFlv extends AnimeScraperModel { - readonly url = "https://animeflv.ws"; + readonly url = "https://m.animeflv.net"; async GetItemInfo(anime: string): Promise { try { const { data } = await axios.get(`${this.url}/anime/${anime}`); const $ = load(data); - const title = $("h2.Title").text().trim(); - const title_alt = $("span.TxtAlt").text().trim(); - const img = $("div.AnimeCover .Image figure img").attr("src"); - const status = $("p.AnmStts span").text().trim(); - const synopsis = $("div.Description").text().trim(); - const episodes = $(".ListCaps li a"); + + //relevant information + const title = $("h1.Title").text().trim(); + const title_alt = title; + const img = $("meta[property='og:image']").attr("content"); + const status = $("p strong.Anm-On").text(); + const synopsis = $("header p:contains(Sinopsis)") + .text() + .replace("Sinopsis:", "") + .trim(); + + //container of episodes + const episodesContainer = $("div.List-Episodes div.AACrdn"); + const AnimeReturn = new AnimeMedia(); AnimeReturn.name = title; AnimeReturn.alt_names = [...title_alt.split(",")]; @@ -38,6 +46,8 @@ export class AnimeFlv extends AnimeScraperModel { AnimeReturn.status = status; AnimeReturn.synopsis = synopsis; AnimeReturn.chronology = []; + AnimeReturn.genres = []; + AnimeReturn.episodes = []; //getRelated $("ul.ListAnmRel li a").each((_i, e) => { @@ -46,26 +56,32 @@ export class AnimeFlv extends AnimeScraperModel { cro.url = `/anime/flv/name/${$(e).attr("href").replace("/anime/", "")}`; AnimeReturn.chronology.push(cro); }); + //get genres - $("nav.Nvgnrs a").each((_i, e) => { - const gen = $(e).text().trim(); + $("footer a").each((_i, e) => { + const gen = $(e).text().trim() as string; + AnimeReturn.genres.push(gen); }); + //get episodes - episodes.each((_i_, e) => { - const l = $(e).attr("href").replace("/", ""); - const episode = new Episode(); - episode.name = $(e).children(".Title").text().trim(); - episode.url = `/anime/flv/episode/${`${l}`.replace( - "/anime", - "/anime/flv" - )}`; - episode.num = Number($(e).children("p").last().text().trim()); - episode.thumbnail.url = $(e) - .children("figure") - .find(".lazy") - .attr("src"); - AnimeReturn.episodes.push(episode); + episodesContainer.each((_i_, e) => { + $(e) + .find("ul li") + .each((_i, e) => { + const link = $(e).find("a"); + const name = link.text().trim(); + const numberEpisode = Number(name.split(" ").slice(-1)); + console.log(numberEpisode); + const episode = new Episode(); + episode.name = name; + episode.url = `/anime/flv/episode/${link + .attr("href") + .replace("/ver/", "")}`; + episode.num = numberEpisode; + + AnimeReturn.episodes.push(episode); + }); }); return AnimeReturn; } catch (error) { @@ -128,10 +144,10 @@ export class AnimeFlv extends AnimeScraperModel { async GetEpisodeServers(episode: string): Promise { try { - const { data } = await axios.get(`${this.url}/${episode}`); + const { data } = await axios.get(`${this.url}/ver/${episode}`); const $ = load(data); - const title = $(".CapiTop").children("h1").text().trim(); - const getLinks = $(".CpCnA .anime_muti_link li"); + const title = $("h1").text().trim(); + const getLinks = $("script"); const numberEpisode = episode.substring(episode.lastIndexOf("-") + 1); const episodeReturn = new Episode(); episodeReturn.name = title; @@ -139,13 +155,39 @@ export class AnimeFlv extends AnimeScraperModel { episodeReturn.num = Number(numberEpisode); episodeReturn.servers = []; - const promises = getLinks.map(async (_i, e) => { - const servers = new EpisodeServer(); - const title = $(e).attr("title"); + getLinks.each((_i, e) => { + interface VideoObject { + title: string; + code: string; + } + + const scriptContent = $(e).html(); + const regexVideoObject = /var videos = (\{.*?\});/s; + + const matchObject = scriptContent.match(regexVideoObject); + + if (matchObject) { + const videoObject: VideoObject[] = JSON.parse(matchObject[1]).SUB; + + for (let index = 0; index < videoObject.length; index++) { + const element = videoObject[index]; + + episodeReturn.servers.push({ + name: element.title, + url: element.code, + }); + } + } + }); + /*const promises = getLinks.map(async (_i, e) => { + /* const servers = new EpisodeServer(); + const title = $(e).find("a").text().trim(); const videoData = $(e).attr("data-video"); servers.name = title; servers.url = videoData; - if (videoData.includes("streaming.php")) { + console.log(title); */ + + /* if (videoData.includes("streaming.php")) { await this.getM3U( `${videoData.replace("streaming.php", "ajax.php")}&refer=none` ).then((g) => { @@ -175,9 +217,9 @@ export class AnimeFlv extends AnimeScraperModel { default: break; } - episodeReturn.servers.push(servers); - }); - await Promise.all(promises); + episodeReturn.servers.push(servers); + })*/ + //await Promise.all(promises); return episodeReturn; } catch (error) { console.log("An error occurred while getting the episode servers", error); @@ -185,7 +227,7 @@ export class AnimeFlv extends AnimeScraperModel { } } - private async getM3U(vidurl: string) { + /* private async getM3U(vidurl: string) { try { const res = await axios.get(vidurl); @@ -193,5 +235,5 @@ export class AnimeFlv extends AnimeScraperModel { } catch (error) { console.log(error); } - } + } */ } From ae9059e2f40d16fdd776dd8eb22b2f9f921445ae Mon Sep 17 00:00:00 2001 From: yako Date: Fri, 19 Jul 2024 14:19:10 -0600 Subject: [PATCH 56/64] up --- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 307eeee2..eb65351c 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -162,7 +162,7 @@ export class AnimeFlv extends AnimeScraperModel { } const scriptContent = $(e).html(); - const regexVideoObject = /var videos = (\{.*?\});/s; + const regexVideoObject = /var videos = (\{.*?\});/; const matchObject = scriptContent.match(regexVideoObject); From 6ffe7016d836b139b36324012332b7aa0ac341c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:30:45 -0400 Subject: [PATCH 57/64] Add: HentaiLa Provider --- src/index.ts | 3 +- .../v1/anime/hentaila/HentaiLaRoutes.ts | 30 ++++ src/scraper/sites/anime/hentaila/HentaiLa.ts | 139 ++++++++++++++++++ src/scraper/sites/manga/comick/Comick.ts | 3 - src/test/Hentaila.spec.ts | 24 +++ 5 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 src/routes/v1/anime/hentaila/HentaiLaRoutes.ts create mode 100644 src/scraper/sites/anime/hentaila/HentaiLa.ts create mode 100644 src/test/Hentaila.spec.ts diff --git a/src/index.ts b/src/index.ts index 7b443354..d47794c9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import tioanime from "../src/routes/v1/anime/tioanime/TioAnimeRoute"; import WcoStream from "../src/routes/v1/anime/wcostream/wcostreamRoutes"; import AnimeBlix from "../src/routes/v1/anime/animeblix/AnimeBlixRoutes"; import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; +import hentaila from "../src/routes/v1/anime/hentaila/HentailaRoutes"; /* Manga */ import comick from "../src/routes/v1/manga/comick/ComickRoutes"; @@ -47,7 +48,7 @@ app.use(tioanime); app.use(WcoStream); app.use(AnimeBlix); app.use(Animevostfr); - +app.use(hentaila); /* anime */ /*Manga*/ diff --git a/src/routes/v1/anime/hentaila/HentaiLaRoutes.ts b/src/routes/v1/anime/hentaila/HentaiLaRoutes.ts new file mode 100644 index 00000000..4cf36bf8 --- /dev/null +++ b/src/routes/v1/anime/hentaila/HentaiLaRoutes.ts @@ -0,0 +1,30 @@ +import { Router } from "express"; +import { HentaiLa } from "../../../../scraper/sites/anime/hentaila/HentaiLa"; +const Anime = new HentaiLa(); +const router = Router(); + +// Filter +router.get("/anime/hentaila/filter", async (req, res) => { + const { search } = req.query; + + const data = await Anime.GetItemByFilter( + search as string + ); + res.send(data); +}); + +// Anime Info +(Episodes list) +router.get("/anime/hentaila/name/:name", async (req, res) => { + const { name } = req.params; + const data = await Anime.GetItemInfo(name); + res.send(data); +}); + +// Episode Info +(Video Servers) +router.get("/anime/hentaila/episode/:episode", async (req, res) => { + const { episode } = req.params; + const data = await Anime.GetEpisodeServers(episode); + res.send(data); +}); + +export default router; diff --git a/src/scraper/sites/anime/hentaila/HentaiLa.ts b/src/scraper/sites/anime/hentaila/HentaiLa.ts new file mode 100644 index 00000000..505c1b22 --- /dev/null +++ b/src/scraper/sites/anime/hentaila/HentaiLa.ts @@ -0,0 +1,139 @@ +import * as cheerio from "cheerio"; +import axios from "axios"; +import { AnimeMedia } from "../../../../types/anime"; +import { Episode, EpisodeServer } from "../../../../types/episode"; + +import { ResultSearch, AnimeResult } from "../../../../types/search"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; + +export class HentaiLa extends AnimeScraperModel { + readonly url = "https://www4.hentaila.com"; + + async GetItemInfo(anime: string): Promise { + try { + const formatUrl = !anime.includes("hentai-") ? "hentai-" + anime : anime; + const { data } = await axios.get(`${this.url}/${formatUrl}`); + const $ = cheerio.load(data); + const genres = []; + $(".genres a").each((_i, e) => genres.push($(e).text())); + + const AnimeInfo: AnimeMedia = { + name: $(".hentai-single .h-header h1.h-title").text().trim(), + url: `/anime/hentaila/name/${anime}`, + synopsis: $(".hentai-single .h-content p").text(), + image: { + url: `${this.url}${$(".hentai-single .h-thumb figure img").attr( + "src" + )}`, + }, + genres: [...genres], + nsfw: true, + type: "Anime", + status: $(".h-meta .status-on").text().includes("En Emision") + ? "En emisión" + : "Finalizado", + episodes: [], + }; + + $(".episodes-list article").each((_i, e) => { + const number = Number( + $(e) + .find("h2.h-title") + .text() + .replace( + $(".hentai-single .h-header h1.h-title").text() + " Episodio", + "" + ) + ); + const AnimeEpisode: Episode = { + name: $(e).find("h2.h-title").text(), + num: number, + thumbnail: { + url: `${this.url}${$(e).find(".h-thumb figure img").attr("src")}`, + }, + url: `/anime/hentaila/episode/${formatUrl}-${number}`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.log(error); + } + } + async GetEpisodeServers(episode: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + const formaturl = anime.includes("hentai-") + ? anime.replace("hentai-", "") + : anime; + const { data } = await axios.get( + `${this.url}/ver/${formaturl}-${number}` + ); + const $ = cheerio.load(data); + + const AnimeEpisodeInfo: Episode = { + name: $(".section-header h1.section-title").text(), + url: `/anime/hentaila/episode/${episode}`, + num: Number(number), + servers: [], + }; + + const video_script = $("script").get().at(-3).children[0].data; + const video_format = eval( + video_script + .slice( + video_script.indexOf("var videos ="), + video_script.lastIndexOf("$(document)") + ) + .replace("var videos =", "") + ); + for (let index = 0; index < video_format.length; index++) { + const Server: EpisodeServer = { + name: video_format[index][0].includes("Yupi") ? "YourUpload" : video_format[index][0], + url: video_format[index][1], + }; + AnimeEpisodeInfo.servers.push(Server); + } + + AnimeEpisodeInfo.servers.sort((a, b) => a.name.localeCompare(b.name)); + return AnimeEpisodeInfo; + } catch (error) { + console.log(error); + } + } + + async GetItemByFilter( + search?: string + ): Promise> { + try { + const content = new FormData(); + content.append("value", search); + const { data } = await axios.post(`${this.url}/api/search`, content); + + const animeSearch: ResultSearch = { + nav: { + count: data.length, + current: 1, + next: 0, + hasNext: false, + }, + results: [], + }; + data.map((e) => { + const animeSearchData: AnimeResult = { + name: e.title, + image: `${this.url}/uploads/portadas/${e.id}.jpg`, + url: `/anime/hentaila/name/${e.slug}`, + type: "Anime", + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.log(error); + } + } +} diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index 37936924..5448d97f 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -78,7 +78,6 @@ export class Comick { return ResultList; } catch (error) { - console.log(error); } } @@ -154,7 +153,6 @@ export class Comick { } return MangaInfo; } catch (error) { - console.log(error); } } @@ -245,7 +243,6 @@ export class Comick { return MangaChapterInfoChapter; } } catch (error) { - console.log(error); } } } diff --git a/src/test/Hentaila.spec.ts b/src/test/Hentaila.spec.ts new file mode 100644 index 00000000..adf27162 --- /dev/null +++ b/src/test/Hentaila.spec.ts @@ -0,0 +1,24 @@ +import { HentaiLa } from "../scraper/sites/anime/hentaila/HentaiLa"; + +describe("AnimeLatinohd", () => { + let hentaila: HentaiLa; + + beforeEach(() => { + hentaila = new HentaiLa(); + }); + + it("should get anime info successfully", async () => { + const animeInfo = await hentaila.GetItemInfo("hentai-korashime-2"); + expect(animeInfo.name).toBe("Korashime 2"); + expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.status).toBe("Finalizado"); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); + }); + + it("should filter anime successfully", async () => { + const result = await hentaila.GetItemByFilter("na"); + expect(result.results.length).toBeGreaterThan(0); + }, 10000); +}); From 158314d339d58ebd2a523820892ffacefd479b8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Fri, 19 Jul 2024 22:38:52 -0400 Subject: [PATCH 58/64] Fix: path Route HentaiLa --- src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index d47794c9..34ea41fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import tioanime from "../src/routes/v1/anime/tioanime/TioAnimeRoute"; import WcoStream from "../src/routes/v1/anime/wcostream/wcostreamRoutes"; import AnimeBlix from "../src/routes/v1/anime/animeblix/AnimeBlixRoutes"; import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; -import hentaila from "../src/routes/v1/anime/hentaila/HentailaRoutes"; +import hentaila from "../src/routes/v1/anime/hentaila/HentaiLaRoutes"; /* Manga */ import comick from "../src/routes/v1/manga/comick/ComickRoutes"; From ba551e1585bb5098c93fe324ebd56ebad561afc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=80=E3=82=A4=E3=83=AC=E3=82=AF=E3=83=88=E3=82=B3?= =?UTF-8?q?=E3=83=8D=E3=82=AF=E3=83=88?= <52444606+TokyoTF@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:22:51 -0400 Subject: [PATCH 59/64] Add HentaiHeven --- src/index.ts | 3 + .../v1/anime/hentaihaven/HentaiHavenRoutes.ts | 31 +++++ .../sites/anime/hentaihaven/HentaiHaven.ts | 131 ++++++++++++++++++ src/test/HentaiHaven.spec.ts | 23 +++ src/test/Hentaila.spec.ts | 2 +- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/routes/v1/anime/hentaihaven/HentaiHavenRoutes.ts create mode 100644 src/scraper/sites/anime/hentaihaven/HentaiHaven.ts create mode 100644 src/test/HentaiHaven.spec.ts diff --git a/src/index.ts b/src/index.ts index 8b5e9ca4..f341031b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,6 +14,7 @@ import WcoStream from "../src/routes/v1/anime/wcostream/wcostreamRoutes"; import AnimeBlix from "../src/routes/v1/anime/animeblix/AnimeBlixRoutes"; import Animevostfr from "../src/routes/v1/anime/animevostfr/AnimevostfrRoutes"; import hentaila from "../src/routes/v1/anime/hentaila/HentaiLaRoutes"; +import HentaiHaven from "../src/routes/v1/anime/hentaihaven/HentaiHavenRoutes"; /* Manga */ import comick from "../src/routes/v1/manga/comick/ComickRoutes"; @@ -44,6 +45,8 @@ app.use(WcoStream); app.use(AnimeBlix); app.use(Animevostfr); app.use(hentaila); +app.use(HentaiHaven); + /* anime */ /*Manga*/ diff --git a/src/routes/v1/anime/hentaihaven/HentaiHavenRoutes.ts b/src/routes/v1/anime/hentaihaven/HentaiHavenRoutes.ts new file mode 100644 index 00000000..8bc25d2d --- /dev/null +++ b/src/routes/v1/anime/hentaihaven/HentaiHavenRoutes.ts @@ -0,0 +1,31 @@ +import { Router } from "express"; +import { HentaiHaven } from "../../../../scraper/sites/anime/hentaihaven/HentaiHaven"; +const Anime = new HentaiHaven(); +const router = Router(); + +// Filter +router.get("/anime/hentaihaven/filter", async (req, res) => { + const { search, page } = req.query; + + const data = await Anime.GetItemByFilter( + search as string, + page as unknown as number + ); + res.send(data); +}); + +// Anime Info +(Episodes list) +router.get("/anime/hentaihaven/name/:name", async (req, res) => { + const { name } = req.params; + const data = await Anime.GetItemInfo(name); + res.send(data); +}); + +// Episode Info +(Video Servers) +router.get("/anime/hentaihaven/episode/:episode", async (req, res) => { + const { episode } = req.params; + const data = await Anime.GetEpisodeServers(episode); + res.send(data); +}); + +export default router; diff --git a/src/scraper/sites/anime/hentaihaven/HentaiHaven.ts b/src/scraper/sites/anime/hentaihaven/HentaiHaven.ts new file mode 100644 index 00000000..8ca5dd1b --- /dev/null +++ b/src/scraper/sites/anime/hentaihaven/HentaiHaven.ts @@ -0,0 +1,131 @@ +import * as cheerio from "cheerio"; +import axios from "axios"; +import { AnimeMedia } from "../../../../types/anime"; +import { Episode, EpisodeServer } from "../../../../types/episode"; + +import { ResultSearch, AnimeResult } from "../../../../types/search"; +import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; + +export class HentaiHaven extends AnimeScraperModel { + readonly url = "https://hentaihaven.com"; + + async GetItemInfo(anime: string): Promise { + try { + const { data } = await axios.get(`${this.url}/video/${anime}`); + const $ = cheerio.load(data); + const genres = []; + + $(".single_data .list") + .first() + .find("a") + .each((_i, e) => genres.push($(e).text())); + + const AnimeInfo: AnimeMedia = { + name: $("h1.htitle").text().trim(), + url: `/anime/hentaihaven/name/${anime}`, + synopsis: $(".vraven_expand .vraven_text.single p") + .last() + .text() + .replace(/\n/g, ""), + alt_names: [ + ...$(".col-12.col-xl-9 h3.h4") + .text() + .split(",") + .map((v) => v.trim()), + ], + image: { + url: $(".hentai_cover .shadow img").attr("src"), + }, + nsfw: true, + genres: [...genres], + episodes: [], + }; + + $(".hentai__episodes ul li").each((_i, e) => { + const number = Number( + $(e).find("a .chapter_info .title").text().replace("Episode ", "") + ); + const AnimeEpisode: Episode = { + name: $(e).find("a .chapter_info .title").text(), + num: number, + thumbnail: { + url: $(e).find("a img").attr("src"), + }, + url: `/anime/hentaihaven/episode/${anime}-${number}`, + }; + + AnimeInfo.episodes.push(AnimeEpisode); + }); + + return AnimeInfo; + } catch (error) { + console.error(error); + } + } + async GetEpisodeServers(episode: string): Promise { + try { + const number = episode.substring(episode.lastIndexOf("-") + 1); + const anime = episode.substring(0, episode.lastIndexOf("-")); + + const { data } = await axios.get( + `${this.url}/video/${anime}/episode-${number}` + ); + const $ = cheerio.load(data); + + const AnimeEpisodeInfo: Episode = { + name: $(".video_footer #chapter-heading").text(), + url: `/anime/hentaihaven/episode/${episode}`, + num: Number(number), + servers: [], + }; + + const Server: EpisodeServer = { + name: "Main Server", + url: $(".player_logic_item iframe").attr("src"), + }; + AnimeEpisodeInfo.servers.push(Server); + + return AnimeEpisodeInfo; + } catch (error) { + console.error(error); + } + } + + async GetItemByFilter( + search?: string, + page: number = 1 + ): Promise> { + try { + const { data } = await axios.get(`${this.url}/page/${page}`, { + params: { + s: search, + }, + }); + const $ = cheerio.load(data); + const count = $(".row_items .item").length; + const animeSearch: ResultSearch = { + nav: { + count: count, + current: Number(page), + next: count < 40 ? 1 : Number(page) + 1, + hasNext: count < 40 ? false : true, + }, + results: [], + }; + $(".row_items .item").each((_i, e) => { + const animeSearchData: AnimeResult = { + name: $(e).find(".title").text(), + image: $(e).find(".cover img").attr("src"), + url: `/anime/hentaiheven/name/${$(e) + .find(".title") + .text() + .replace(this.url + "/video", "")}`, + }; + animeSearch.results.push(animeSearchData); + }); + return animeSearch; + } catch (error) { + console.error(error); + } + } +} diff --git a/src/test/HentaiHaven.spec.ts b/src/test/HentaiHaven.spec.ts new file mode 100644 index 00000000..59c7d848 --- /dev/null +++ b/src/test/HentaiHaven.spec.ts @@ -0,0 +1,23 @@ +import { HentaiHaven } from "../scraper/sites/anime/hentaihaven/HentaiHaven"; + +describe("HentaiHaven", () => { + let hentaihaven: HentaiHaven; + + beforeEach(() => { + hentaihaven = new HentaiHaven(); + }); + + it("should get anime info successfully", async () => { + const animeInfo = await hentaihaven.GetItemInfo("tokubetsu-jugyou-2"); + expect(animeInfo.name).toBe("Tokubetsu Jugyou 2"); + expect(animeInfo.image.url).toContain(".jpg"); + expect(animeInfo.synopsis?.length).toBeGreaterThan(0); + expect(animeInfo.genres?.length).toBeGreaterThan(0); + expect(animeInfo.episodes?.length).toBeGreaterThan(0); + }); + + it("should filter anime successfully", async () => { + const result = await hentaihaven.GetItemByFilter("animation"); + expect(result.results.length).toBeGreaterThan(0); + }, 10000); +}); diff --git a/src/test/Hentaila.spec.ts b/src/test/Hentaila.spec.ts index adf27162..6b3749b1 100644 --- a/src/test/Hentaila.spec.ts +++ b/src/test/Hentaila.spec.ts @@ -1,6 +1,6 @@ import { HentaiLa } from "../scraper/sites/anime/hentaila/HentaiLa"; -describe("AnimeLatinohd", () => { +describe("HentaiLa", () => { let hentaila: HentaiLa; beforeEach(() => { From 6b873e750e68fb3956bb119eb944b748faa5df62 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 27 Jul 2024 21:13:47 -0500 Subject: [PATCH 60/64] fix(providers): get info routes outdated --- src/routes/v1/manga/comick/ComickRoutes.ts | 2 +- src/routes/v1/manga/inmanga/InmangaRoutes.ts | 2 +- src/routes/v1/manga/manganelo/ManganeloRoutes.ts | 2 +- src/routes/v1/manga/mangareader/MangaReaderRoutes.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/routes/v1/manga/comick/ComickRoutes.ts b/src/routes/v1/manga/comick/ComickRoutes.ts index f6bf3895..13f6d337 100644 --- a/src/routes/v1/manga/comick/ComickRoutes.ts +++ b/src/routes/v1/manga/comick/ComickRoutes.ts @@ -18,7 +18,7 @@ router.get("/manga/comick/filter", async (req, res) => { res.send(data); }); -router.get("/manga/comick/title/:manga", async (req, res) => { +router.get("/manga/comick/name/:manga", async (req, res) => { const { manga } = req.params; const { lang } = req.query; diff --git a/src/routes/v1/manga/inmanga/InmangaRoutes.ts b/src/routes/v1/manga/inmanga/InmangaRoutes.ts index 55594112..dab41292 100644 --- a/src/routes/v1/manga/inmanga/InmangaRoutes.ts +++ b/src/routes/v1/manga/inmanga/InmangaRoutes.ts @@ -14,7 +14,7 @@ router.get("/manga/inmanga/filter", async (req, res) => { res.send(data); }); -router.get("/manga/inmanga/title/:manga", async (req, res) => { +router.get("/manga/inmanga/name/:manga", async (req, res) => { const { manga } = req.params; const { cid } = req.query; diff --git a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts index 2716a7ba..8f80e403 100644 --- a/src/routes/v1/manga/manganelo/ManganeloRoutes.ts +++ b/src/routes/v1/manga/manganelo/ManganeloRoutes.ts @@ -5,7 +5,7 @@ import { Manganelo } from "../../../../scraper/sites/manga/manganelo/Manganelo"; const router = Router(); const manganelo = new Manganelo(); -router.get(`/manga/${manganelo.name}/title/:id`, async (req, res) => { +router.get(`/manga/${manganelo.name}/name/:id`, async (req, res) => { const result = await manganelo.GetItemInfo( req.params.id as unknown as string, ); diff --git a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts index 04419ff5..b4b93ca1 100644 --- a/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts +++ b/src/routes/v1/manga/mangareader/MangaReaderRoutes.ts @@ -12,7 +12,7 @@ import { const mangaReader = new MangaReader(); const router = Router(); -router.get("/manga/mangareader/title/:id", async (req, res) => { +router.get("/manga/mangareader/name/:id", async (req, res) => { try { const id = req.params.id as unknown as string; From 20bf28fb4229d47e9035512209392e7fdab92985 Mon Sep 17 00:00:00 2001 From: barrientosvctor Date: Sat, 27 Jul 2024 22:25:12 -0500 Subject: [PATCH 61/64] fix(providers): outdated getinfo api url in migrated provider source code --- src/scraper/sites/manga/MangaReader/MangaReader.ts | 2 +- src/scraper/sites/manga/comick/Comick.ts | 4 ++-- src/scraper/sites/manga/inmanga/Inmanga.ts | 6 +++--- src/scraper/sites/manga/manganelo/Manganelo.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/scraper/sites/manga/MangaReader/MangaReader.ts b/src/scraper/sites/manga/MangaReader/MangaReader.ts index ab69061a..584219db 100644 --- a/src/scraper/sites/manga/MangaReader/MangaReader.ts +++ b/src/scraper/sites/manga/MangaReader/MangaReader.ts @@ -327,7 +327,7 @@ export class MangaReader extends MangaScraperModel { id: mangaResultsID, name: mangaResultsTitle, thumbnail: new Image(mangaResultsThumbnail), - url: `/manga/mangareader/title/${mangaResultsID}`, + url: `/manga/mangareader/name/${mangaResultsID}`, }); }); diff --git a/src/scraper/sites/manga/comick/Comick.ts b/src/scraper/sites/manga/comick/Comick.ts index 5448d97f..505462ba 100644 --- a/src/scraper/sites/manga/comick/Comick.ts +++ b/src/scraper/sites/manga/comick/Comick.ts @@ -70,7 +70,7 @@ export class Comick { thumbnail: { url: "https://meo.comick.pictures/" + e.md_covers[0].b2key, }, - url: `/manga/comick/title/${e.slug}`, + url: `/manga/comick/name/${e.slug}`, }; ResultList.results.push(ListMangaResult); } @@ -103,7 +103,7 @@ export class Comick { alt_names: mangaInfoParseObj.comic.md_titles.map( (e: { title: string }) => e.title ), - url: `/manga/comick/title/${mangaInfoParseObj.comic.slug}`, + url: `/manga/comick/name/${mangaInfoParseObj.comic.slug}`, synopsis: mangaInfoParseObj.comic.desc, nsfw: mangaInfoParseObj.comic.hentai, langlist: mangaInfoParseObj.langList, diff --git a/src/scraper/sites/manga/inmanga/Inmanga.ts b/src/scraper/sites/manga/inmanga/Inmanga.ts index fc35f3d1..a2729793 100644 --- a/src/scraper/sites/manga/inmanga/Inmanga.ts +++ b/src/scraper/sites/manga/inmanga/Inmanga.ts @@ -123,7 +123,7 @@ export class Inmanga { }, // old version `/manga/inmanga/title/${title.replace(/[^a-zA-Z:]/g, "-")}` - url: `/manga/inmanga/title/${name}?cid=${cid}`, + url: `/manga/inmanga/name/${name}?cid=${cid}`, }; ResultList.results.push(ListMangaResult); }); @@ -144,7 +144,7 @@ export class Inmanga { id: cid, name: $_("div.col-md-3.col-sm-4 div.panel-heading.visible-xs").text(), alt_names: AltNames, - url: `/manga/inmanga/title/${manga}`, + url: `/manga/inmanga/name/${manga}`, synopsis: $_( "body > div > section > div > div > div:nth-child(6) > div > div.panel-body" ) @@ -168,7 +168,7 @@ export class Inmanga { ).each((_i, e) => AltNames.push($_(e).text().replace(";", "")) ); - + $_( ".col-md-9.col-sm-8.col-xs-12 .panel.widget .panel-heading .label.ml-sm" ).each((_i, e) => MangaInfo.genres.push($_(e).text().trim())); diff --git a/src/scraper/sites/manga/manganelo/Manganelo.ts b/src/scraper/sites/manga/manganelo/Manganelo.ts index aff80ac5..d271338b 100644 --- a/src/scraper/sites/manga/manganelo/Manganelo.ts +++ b/src/scraper/sites/manga/manganelo/Manganelo.ts @@ -104,7 +104,7 @@ export class Manganelo extends MangaScraperModel { const mangaInfoResults: IMangaResult = { id: mangaResultId, name: name, - url: `/manga/${this.name}/title/${mangaResultId}`, + url: `/manga/${this.name}/name/${mangaResultId}`, }; return mangaInfoResults; @@ -152,7 +152,7 @@ export class Manganelo extends MangaScraperModel { .get(); manga.id = mangaId; - manga.url = `/manga/${this.name}/title/${mangaId}`; + manga.url = `/manga/${this.name}/name/${mangaId}`; manga.name = title; manga.alt_names = Array.of(altTitle); manga.thumbnail = new Image(thumbnail); From 5173390c3f8d0aa3f140938a74418a938e5fac52 Mon Sep 17 00:00:00 2001 From: yako Date: Mon, 23 Sep 2024 20:32:29 -0600 Subject: [PATCH 62/64] faltan cambios en el test --- .../v1/anime/animeflv/AnimeflvRoutes.ts | 6 +- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 55 ++++++---- .../sites/anime/animeflv/animeflv_helper.ts | 101 ++++++++++-------- src/test/Animeflv.spec.ts | 50 ++++++--- 4 files changed, 129 insertions(+), 83 deletions(-) diff --git a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts index 3c671f9b..1ec8d9fb 100644 --- a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts +++ b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts @@ -38,7 +38,7 @@ r.get("/anime/flv/episode/:episode", async (req, res) => { r.get("/anime/flv/filter", async (req, res) => { try { const gen = req.query.gen as Genres; - const date = req.query.date as string; + const year = req.query.year as string; const type = req.query.type as TypeAnimeflv; const status = req.query.status as StatusAnimeflv; const ord = req.query.ord as OrderAnimeflv; @@ -48,12 +48,12 @@ r.get("/anime/flv/filter", async (req, res) => { const flv = new AnimeFlv(); const animeInfo = await flv.GetItemByFilter( gen, - date, + year, type, status, ord, page, - title, + title ); res.send(animeInfo); } catch (error) { diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index eb65351c..2c15cf5f 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios, { AxiosResponse } from "axios"; import { load } from "cheerio"; import { AnimeMedia, Chronology } from "../../../../types/anime"; import { Episode } from "../../../../types/episode"; @@ -72,7 +72,6 @@ export class AnimeFlv extends AnimeScraperModel { const link = $(e).find("a"); const name = link.text().trim(); const numberEpisode = Number(name.split(" ").slice(-1)); - console.log(numberEpisode); const episode = new Episode(); episode.name = name; episode.url = `/anime/flv/episode/${link @@ -97,7 +96,7 @@ export class AnimeFlv extends AnimeScraperModel { async GetItemByFilter( gen?: Genres | string, - date?: string, + year?: string, type?: TypeAnimeflv, status?: StatusAnimeflv, ord?: OrderAnimeflv, @@ -105,29 +104,35 @@ export class AnimeFlv extends AnimeScraperModel { title?: string ): Promise> { try { - const { data } = await axios.get(`${this.url}/browse`, { - params: { - genres: gen || "all", - year: date || "all", - status: status || "all", - Tipo: type || "all", - order: ord || 1, - page: page || 1, - q: title, - }, - }); + const { data, request }: AxiosResponse = await axios.get( + `${this.url}/browse`, + { + params: { + page: page, + genre: gen, + year: year, + status: status, + type: type, + order: ord, + q: title, + }, + } + ); + console.log(request); const $ = load(data); - const infoList = $("ul.ListAnimes li"); + const infoList = $("ul.List-Animes li"); const data_filter = new ResultSearch(); data_filter.results = []; infoList.each((_i, e) => { const info = new AnimeResult(); - info.name = $(e).find("h3").text().trim(); - info.image = - $(e) - .find("a") - .attr("href") - .replace("/anime/", "https://img.animeflv.ws/cover/") + ".jpg"; + info.name = $(e).find("h2").text().trim(); + info.image = $(e) + .find("img") + .attr("src") + .replace( + "/uploads/animes/", + "https://m.animeflv.net/uploads/animes/" + ); info.url = `/anime/flv/name/${$(e) .find("a") .attr("href") @@ -145,6 +150,10 @@ export class AnimeFlv extends AnimeScraperModel { async GetEpisodeServers(episode: string): Promise { try { const { data } = await axios.get(`${this.url}/ver/${episode}`); + /* const test: AxiosResponse = await axios.get( + "https://streamtape.com/e/ybVywBZRXMheQ2/" + ); + const $t = load(test.data); */ const $ = load(data); const title = $("h1").text().trim(); const getLinks = $("script"); @@ -155,6 +164,10 @@ export class AnimeFlv extends AnimeScraperModel { episodeReturn.num = Number(numberEpisode); episodeReturn.servers = []; + /* const player = $t("div.plyr__video-wrapper").html(); + + console.log(player); */ + getLinks.each((_i, e) => { interface VideoObject { title: string; diff --git a/src/scraper/sites/anime/animeflv/animeflv_helper.ts b/src/scraper/sites/anime/animeflv/animeflv_helper.ts index 601b7f57..4ea4c5bd 100644 --- a/src/scraper/sites/anime/animeflv/animeflv_helper.ts +++ b/src/scraper/sites/anime/animeflv/animeflv_helper.ts @@ -1,52 +1,63 @@ //genres animeflf export enum Genres { - Action = "Acción", - MartialArts = "Artes Marciales", - Adventure = "Aventuras", - Racing = "Carreras", - ScienceFiction = "Ciencia Ficción", - Comedy = "Comedia", - Dementia = "Demencia", - Demons = "Demonios", - Sports = "Deportes", - Drama = "Drama", - Ecchi = "Ecchi", - School = "Escolares", - Space = "Espacial", - Fantasy = "Fantasía", - Harem = "Harem", - Historical = "Histórico", - Kids = "Infantil", - Josei = "Josei", - Games = "Juegos", - Magic = "Magia", - Mecha = "Mecha", - Military = "Militar", - Mystery = "Misterio", - Music = "Música", - Parody = "Parodia", - Police = "Policía", - Psychological = "Psicológico", - SliceOfLife = "Recuentos de la vida", - Romance = "Romance", - Samurai = "Samurai", - Seinen = "Seinen", - Shoujo = "Shoujo", - Shounen = "Shounen", - Supernatural = "Sobrenatural", - Superpowers = "Superpoderes", - Suspense = "Suspenso", - Horror = "Terror", - Vampires = "Vampiros", - Yaoi = "Yaoi", - Yuri = "Yuri", + Action = "accion", + MartialArts = "artes-marciales", + Adventure = "aventura", + Racing = "carreras", + ScienceFiction = "ciencia-ficcion", + Comedy = "comedia", + Dementia = "demencia", + Demons = "demonios", + Sports = "deportes", + Drama = "drama", + Ecchi = "ecchi", + School = "escolares", + Space = "espacial", + Fantasy = "fantasia", + Harem = "harem", + Historical = "historico", + Kids = "infantil", + Josei = "josei", + Games = "juegos", + Magic = "magia", + Mecha = "mecha", + Military = "militar", + Mystery = "misterio", + Music = "musica", + Parody = "parodia", + Police = "policia", + Psychological = "psicologico", + SliceOfLife = "recuentos-de-la-vida", + Romance = "romance", + Samurai = "samurai", + Seinen = "seinen", + Shoujo = "shoujo", + Shounen = "shounen", + Supernatural = "sobrenatural", + Superpowers = "superpoderes", + Suspense = "suspenso", + Horror = "terror", + Vampires = "vampiros", + Yaoi = "yaoi", + Yuri = "yuri", } export enum StatusAnimeflv { - OnGoing = "En emision", - Finished = "Finalizado", - Upcoming = "Próximamente", + OnGoing = "1", + Finished = "2", + Upcoming = "3", } -export type TypeAnimeflv = "all" | 1 | 2 | 3 | 4; -export type OrderAnimeflv = "all" | 1 | 2 | 3 | 4 | 5; +export enum TypeAnimeflv { + TV = "tv", + MOVIE = "movie", + SPECIAL = "special", + OVA = "ova", +} +export enum OrderAnimeflv { + DEFAULT = "default", + UPDATED = "updated", + ADDED = "added", + TITLE = "title", + RATING = "rating", +} diff --git a/src/test/Animeflv.spec.ts b/src/test/Animeflv.spec.ts index 24b09879..d3fea662 100644 --- a/src/test/Animeflv.spec.ts +++ b/src/test/Animeflv.spec.ts @@ -1,29 +1,39 @@ import { AnimeFlv } from "../scraper/sites/anime/animeflv/AnimeFlv"; import { AnimeMedia } from "../types/anime"; -import { Episode } from "../types/episode"; +/* import { Episode } from "../types/episode"; +import { + Genres, + StatusAnimeflv, +} from "../scraper/sites/anime/animeflv/animeflv_helper"; */ import { Genres, StatusAnimeflv, } from "../scraper/sites/anime/animeflv/animeflv_helper"; -describe("AnimeFlv", () => { +describe("AnimeFlv test", () => { let animeFlv: AnimeFlv; beforeEach(() => { animeFlv = new AnimeFlv(); }); - it("should get anime info successfully", async () => { - const animeInfo: AnimeMedia = - await animeFlv.GetItemInfo("25jigen-no-ririsa"); - expect(animeInfo.name).toBe("Wonder Egg Priority"); - expect(animeInfo.alt_names).toContain("ワンダーエッグ・プライオリティ"); - expect(animeInfo.image.url).toContain(".jpg"); - expect(animeInfo.status).toBe("Finalizado"); - expect(animeInfo.synopsis?.length).toBeGreaterThan(0); - expect(animeInfo.chronology?.length).toBeGreaterThan(0); - expect(animeInfo.genres?.length).toBeGreaterThan(0); - expect(animeInfo.episodes?.length).toBeGreaterThan(0); + test("should get anime info successfully", async () => { + const animeInfo: AnimeMedia = await animeFlv.GetItemInfo("horimiya-piece"); + + const alt_names_expected: string[] = ["Horimiya: Piece"]; + const genres_expected: string[] = ["Escolares", "Romance", "Shounen"]; + + expect(animeInfo.name).toBe("Horimiya: Piece"); + expect(animeInfo.alt_names).toEqual( + expect.arrayContaining(alt_names_expected) + ); + expect(animeInfo.image.url).toContain(".jp"); + expect(animeInfo.synopsis).toBe( + "Historias del manga no adaptadas en el anime principal." + ); + expect(animeInfo.chronology?.length).toBeGreaterThanOrEqual(0); + expect(animeInfo.genres).toEqual(expect.arrayContaining(genres_expected)); + expect(animeInfo.episodes?.length).toBeGreaterThanOrEqual(12); }); it("should filter anime successfully", async () => { @@ -38,6 +48,18 @@ describe("AnimeFlv", () => { expect(result.results.length).toBeGreaterThan(0); }); + /* it("should filter anime successfully", async () => { + const result = await animeFlv.GetItemByFilter( + Genres.Action, + "all", + "all", + StatusAnimeflv.OnGoing, + 1, + 1 + ); + expect(result.results.length).toBeGreaterThan(0); + }); + it("should get episode servers successfully", async () => { const episode: Episode = await animeFlv.GetEpisodeServers( "wonder-egg-priority-01" @@ -46,5 +68,5 @@ describe("AnimeFlv", () => { expect(episode.url).toContain("/anime/flv/episode/wonder-egg-priority-01"); expect(episode.num).toBe(1); expect(episode?.servers?.length).toBeGreaterThan(0); - }); + }); */ }); From ecb50bf7e86a7e2a22df8673be33986424ab3b24 Mon Sep 17 00:00:00 2001 From: yako Date: Tue, 31 Dec 2024 11:38:15 -0600 Subject: [PATCH 63/64] feature: all done --- src/routes/v1/anime/animeflv/AnimeflvRoutes.ts | 16 ++++++++++------ src/scraper/sites/anime/animeflv/AnimeFlv.ts | 10 ++++++---- src/utils/ScraperError.ts | 10 ++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 src/utils/ScraperError.ts diff --git a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts index 1ec8d9fb..139eb54a 100644 --- a/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts +++ b/src/routes/v1/anime/animeflv/AnimeflvRoutes.ts @@ -6,6 +6,7 @@ import { StatusAnimeflv, OrderAnimeflv, } from "../../../../scraper/sites/anime/animeflv/animeflv_helper"; +import { ScraperErrorResponse } from "utils/ScraperError"; const r = Router(); //anime info @@ -16,8 +17,9 @@ r.get("/anime/flv/name/:name", async (req, res) => { const animeInfo = await flv.GetItemInfo(name); res.send(animeInfo); } catch (error) { - console.log(error); - res.status(500).send(error); + if (error instanceof ScraperErrorResponse) { + res.status(404).send(error); + } } }); @@ -29,8 +31,9 @@ r.get("/anime/flv/episode/:episode", async (req, res) => { const animeInfo = await flv.GetEpisodeServers(episode); res.send(animeInfo); } catch (error) { - console.log(error); - res.status(500).send(error); + if (error instanceof ScraperErrorResponse) { + res.status(404).send(error); + } } }); @@ -57,8 +60,9 @@ r.get("/anime/flv/filter", async (req, res) => { ); res.send(animeInfo); } catch (error) { - console.log(error); - res.status(500).send(error); + if (error instanceof ScraperErrorResponse) { + res.status(404).send(error); + } } }); diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index 2c15cf5f..ccd224b4 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -15,6 +15,7 @@ import { AnimeResult, } from "../../../../types/search"; import { AnimeScraperModel } from "../../../../models/AnimeScraperModel"; +import { ScraperErrorResponse } from "utils/ScraperError"; export class AnimeFlv extends AnimeScraperModel { readonly url = "https://m.animeflv.net"; @@ -88,8 +89,9 @@ export class AnimeFlv extends AnimeScraperModel { "An error occurred while getting the anime info: invalid name", error ); - throw new Error( - "An error occurred while getting the anime info: invalid name" + throw new ScraperErrorResponse( + "An error occurred while getting the anime info: invalid name", + 404 ); } } @@ -143,7 +145,7 @@ export class AnimeFlv extends AnimeScraperModel { return data_filter; } catch (error) { console.log("An error occurred while getting the filter values", error); - throw new Error("An error occurred while getting the filter values"); + throw new ScraperErrorResponse("An error occurred while getting the filter values", 404); } } @@ -236,7 +238,7 @@ export class AnimeFlv extends AnimeScraperModel { return episodeReturn; } catch (error) { console.log("An error occurred while getting the episode servers", error); - throw new Error("An error occurred while getting the episode servers"); + throw new ScraperErrorResponse("An error occurred while getting the episode servers",404); } } diff --git a/src/utils/ScraperError.ts b/src/utils/ScraperError.ts new file mode 100644 index 00000000..9d10317b --- /dev/null +++ b/src/utils/ScraperError.ts @@ -0,0 +1,10 @@ +import { AxiosError } from "axios"; + +export class ScraperErrorResponse extends AxiosError { + constructor(message: string, status: number, name?: string) { + super(message); + this.name = name || "Server error"; + this.stack = null; + this.code = status.toString(); + } +} From c69166ef681eccc11fde743178baef84acc8b7ec Mon Sep 17 00:00:00 2001 From: yako Date: Tue, 31 Dec 2024 11:50:14 -0600 Subject: [PATCH 64/64] fix --- src/scraper/sites/anime/animeflv/AnimeFlv.ts | 7 +++---- src/utils/ScraperError.ts | 3 +-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/scraper/sites/anime/animeflv/AnimeFlv.ts b/src/scraper/sites/anime/animeflv/AnimeFlv.ts index ccd224b4..c89df662 100644 --- a/src/scraper/sites/anime/animeflv/AnimeFlv.ts +++ b/src/scraper/sites/anime/animeflv/AnimeFlv.ts @@ -90,8 +90,7 @@ export class AnimeFlv extends AnimeScraperModel { error ); throw new ScraperErrorResponse( - "An error occurred while getting the anime info: invalid name", - 404 + "An error occurred while getting the anime info: invalid name" ); } } @@ -145,7 +144,7 @@ export class AnimeFlv extends AnimeScraperModel { return data_filter; } catch (error) { console.log("An error occurred while getting the filter values", error); - throw new ScraperErrorResponse("An error occurred while getting the filter values", 404); + throw new ScraperErrorResponse("An error occurred while getting the filter values"); } } @@ -238,7 +237,7 @@ export class AnimeFlv extends AnimeScraperModel { return episodeReturn; } catch (error) { console.log("An error occurred while getting the episode servers", error); - throw new ScraperErrorResponse("An error occurred while getting the episode servers",404); + throw new ScraperErrorResponse("An error occurred while getting the episode servers"); } } diff --git a/src/utils/ScraperError.ts b/src/utils/ScraperError.ts index 9d10317b..86c0083b 100644 --- a/src/utils/ScraperError.ts +++ b/src/utils/ScraperError.ts @@ -1,10 +1,9 @@ import { AxiosError } from "axios"; export class ScraperErrorResponse extends AxiosError { - constructor(message: string, status: number, name?: string) { + constructor(message: string, name?: string) { super(message); this.name = name || "Server error"; this.stack = null; - this.code = status.toString(); } }