├── .npmrc ├── .npmignore ├── .gitignore ├── src ├── utils │ ├── index.ts │ ├── executeSearch.ts │ ├── getPrevAndNextPages.ts │ └── scrapAnimeData.ts ├── functions │ ├── index.ts │ ├── searchAnimesBySpecificURL.ts │ ├── searchAnime.ts │ ├── getOnAir.ts │ ├── getLatest.ts │ ├── getAnimeInfo.ts │ └── searchAnimesByFilter.ts ├── index.ts ├── constants │ └── index.ts └── types │ └── index.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── test └── test.ts └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | tsconfig.json 4 | test.ts -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist 4 | test/*.js 5 | .vscode -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./scrapAnimeData"; 2 | export * from "./getPrevAndNextPages"; 3 | export * from "./executeSearch"; -------------------------------------------------------------------------------- /src/functions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./getAnimeInfo" 2 | export * from "./getLatest" 3 | export * from "./getOnAir" 4 | export * from "./searchAnime" 5 | export * from "./searchAnimesBySpecificURL" 6 | export * from "./searchAnimesByFilter" -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AnimeData, SearchAnimeResults, AnimeGenre, AnimeOnAirData, AnimeStatus, AnimeType, ChapterData, PartialAnimeData, 3 | FilterOptions, FilterAnimeResults, FilterOrderType 4 | } from "./types" 5 | export * from "./constants" 6 | export * from "./functions" -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*.ts", "src/*.d.ts"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "types": ["mocha"], 8 | "noImplicitAny": true, 9 | "outDir": "./dist", 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "watch": false, 13 | "strict": true, 14 | "skipLibCheck": true, 15 | "typeRoots": ["./src/types"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 MixDevCode 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -------------------------------------------------------------------------------- /src/functions/searchAnimesBySpecificURL.ts: -------------------------------------------------------------------------------- 1 | import cloudscraper from "cloudscraper"; 2 | import { CloudscraperOptions } from "../constants"; 3 | import { type SearchAnimeResults } from "../types"; 4 | import { executeSearch } from "../utils"; 5 | 6 | export async function searchAnimesBySpecificURL(url: string): Promise { 7 | 8 | if (!url || (typeof url) !== "string") 9 | throw new TypeError(`Parámetro url debe ser una string no vacía, pasaste: ${url}`, { cause: "url is not a valid url." }); 10 | 11 | try { 12 | CloudscraperOptions.uri = url; 13 | 14 | const specificData = (await cloudscraper(CloudscraperOptions)) as string; 15 | 16 | return executeSearch(specificData) 17 | } 18 | catch { 19 | return null; 20 | } 21 | } -------------------------------------------------------------------------------- /src/functions/searchAnime.ts: -------------------------------------------------------------------------------- 1 | import cloudscraper from "cloudscraper"; 2 | import { CloudscraperOptions } from "../constants"; 3 | import { SearchAnimeResults } from "../types"; 4 | import { executeSearch } from "../utils"; 5 | 6 | export async function searchAnime(query: string): Promise { 7 | 8 | if (!query || (typeof query) !== "string") 9 | throw new TypeError(`El parámetro query debe ser una string no vacía, pasaste: ${query}`, { cause: `query: ${query}` }); 10 | 11 | try { 12 | CloudscraperOptions.uri = 'https://www3.animeflv.net/browse?q=' + query.toLowerCase().replace(/\s+/g, "+"); 13 | 14 | const searchData = (await cloudscraper(CloudscraperOptions)) as string; 15 | 16 | return executeSearch(searchData) 17 | 18 | } catch { 19 | return null; 20 | } 21 | } -------------------------------------------------------------------------------- /src/utils/executeSearch.ts: -------------------------------------------------------------------------------- 1 | import { type SearchAnimeResults } from "../types"; 2 | import { load } from "cheerio" 3 | import { getNextAndPrevPages } from "./getPrevAndNextPages"; 4 | import { scrapSearchAnimeData } from "./scrapAnimeData"; 5 | 6 | export function executeSearch(searchData: string): SearchAnimeResults | null { 7 | 8 | const $ = load(searchData); 9 | 10 | const search: SearchAnimeResults = { 11 | previousPage: null, 12 | nextPage: null, 13 | foundPages: 0, 14 | data: [] 15 | } 16 | 17 | const pageSelector = $('body > div.Wrapper > div > div > main > div > ul > li'); 18 | const { foundPages, nextPage, previousPage } = getNextAndPrevPages(pageSelector); 19 | 20 | search.data = scrapSearchAnimeData($) 21 | search.foundPages = foundPages; 22 | search.nextPage = nextPage; 23 | search.previousPage = previousPage; 24 | 25 | return search; 26 | } -------------------------------------------------------------------------------- /src/utils/getPrevAndNextPages.ts: -------------------------------------------------------------------------------- 1 | import { Cheerio, type Element } from "cheerio"; 2 | 3 | export function getNextAndPrevPages(selector: Cheerio): { 4 | foundPages: number 5 | previousPage: string | null 6 | nextPage: string | null 7 | } { 8 | const aTagValue = selector.last().prev().find('a').text(); 9 | const aRef = selector.eq(0).children('a').attr('href'); 10 | 11 | let foundPages = 0; 12 | let previousPage: string | null = ""; 13 | let nextPage: string | null = ""; 14 | 15 | if (Number(aTagValue) === 0) foundPages = 1; 16 | else foundPages = Number(aTagValue); 17 | 18 | if (aRef === "#" || foundPages == 1) previousPage = null; 19 | else previousPage = 'https://www3.animeflv.net' + aRef; 20 | 21 | if (selector.last().children('a').attr('href') === "#" || foundPages == 1) nextPage = null; 22 | else nextPage = 'https://www3.animeflv.net' + selector.last().children('a').attr('href'); 23 | 24 | return { foundPages, nextPage, previousPage } 25 | 26 | } -------------------------------------------------------------------------------- /src/utils/scrapAnimeData.ts: -------------------------------------------------------------------------------- 1 | import { type CheerioAPI } from "cheerio" 2 | import type { AnimeType, PartialAnimeData } from "../types"; 3 | 4 | export function scrapSearchAnimeData($: CheerioAPI): PartialAnimeData[] { 5 | 6 | const selectedElement = $('body > div.Wrapper > div > div > main > ul > li') 7 | 8 | if (selectedElement.length > 0) { 9 | const data: PartialAnimeData[] = [] 10 | 11 | selectedElement.each((i, el) => { 12 | data.push({ 13 | title: $(el).find('h3').text(), 14 | cover: $(el).find('figure > img').attr('src')!, 15 | synopsis: $(el).find('div.Description > p').eq(1).text(), 16 | rating: $(el).find('article > div > p:nth-child(2) > span.Vts.fa-star').text(), 17 | id: $(el).find('a').attr('href')!.replace("/anime/", ""), 18 | type: $(el).find('a > div > span.Type').text() as AnimeType, 19 | url: 'https://www3.animeflv.net' + ($(el).find('a').attr('href') as string), 20 | }); 21 | }); 22 | 23 | return data; 24 | 25 | } else { 26 | return []; 27 | } 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "animeflv-api", 3 | "version": "2.0.0", 4 | "description": "Un web-scrapper cuya función es obtener información del sitio conocido como AnimeFLV.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "start": "node dist/index.js", 9 | "test": "mocha -r ts-node/register ./test/test.ts", 10 | "build": "tsc" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/MixDevCode/animeflv-api.git" 15 | }, 16 | "keywords": [ 17 | "scrapper", 18 | "animeflv", 19 | "anime" 20 | ], 21 | "contributors": [ 22 | { 23 | "name": "MixDevCode", 24 | "url": "https://github.com/MixDevCode/" 25 | }, 26 | { 27 | "name": "ShompiFlen", 28 | "url": "https://github.com/Shompi" 29 | } 30 | ], 31 | "license": "ISC", 32 | "engines": { 33 | "node": ">=16.0.0" 34 | }, 35 | "dependencies": { 36 | "cheerio": "^1.0.0-rc.12", 37 | "cloudscraper": "^4.6.0" 38 | }, 39 | "devDependencies": { 40 | "@types/chai": "^4.3.4", 41 | "@types/mocha": "^10.0.1", 42 | "chai": "^4.3.7", 43 | "mocha": "^10.2.0", 44 | "ts-node": "^10.9.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/functions/getOnAir.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import cloudscraper from "cloudscraper"; 3 | import { CloudscraperOptions } from "../constants"; 4 | import { AnimeOnAirData, AnimeType } from "../types"; 5 | 6 | export async function getOnAir(): Promise { 7 | try { 8 | 9 | CloudscraperOptions.uri = 'https://www3.animeflv.net/'; 10 | 11 | const onAirData = (await cloudscraper(CloudscraperOptions)) as string; 12 | const $ = load(onAirData); 13 | 14 | const onAir: AnimeOnAirData[] = [] 15 | if ($('.ListSdbr > li').length > 0) { 16 | $('.ListSdbr > li').each((i, el) => { 17 | const temp: AnimeOnAirData = { 18 | title: $(el).find('a').remove('span').text(), 19 | type: $(el).find('a').children('span').text() as AnimeType, 20 | id: $(el).find('a').attr('href')!.replace("/anime/", ""), 21 | url: 'https://www3.animeflv.net' + $(el).find('a').attr('href') as string 22 | } 23 | 24 | onAir.push(temp); 25 | }); 26 | } 27 | 28 | return onAir; 29 | 30 | } catch { 31 | return []; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/functions/getLatest.ts: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import cloudscraper from "cloudscraper"; 3 | import { CloudscraperOptions } from "../constants"; 4 | import { ChapterData } from "../types"; 5 | 6 | export async function getLatest(): Promise { 7 | try { 8 | 9 | CloudscraperOptions.uri = 'https://www3.animeflv.net/'; 10 | 11 | const chaptersData = (await cloudscraper(CloudscraperOptions)) as string; 12 | const $ = load(chaptersData); 13 | 14 | const chapterSelector = $('body > div.Wrapper > div > div > div > main > ul.ListEpisodios.AX.Rows.A06.C04.D03 > li'); 15 | 16 | const chapters: ChapterData[] = [] 17 | if (chapterSelector.length > 0) { 18 | 19 | chapterSelector.each((i, el) => { 20 | chapters.push({ 21 | title: $(el).find('strong').text(), 22 | chapter: Number($(el).find('span.Capi').text().replace("Episodio ", "")), 23 | cover: 'https://animeflv.net' + ($(el).find('img').attr('src') as string), 24 | url: 'https://www3.animeflv.net' + $(el).find('a').attr('href') as string 25 | }); 26 | }); 27 | 28 | } 29 | 30 | return chapters; 31 | 32 | } catch { 33 | return []; 34 | } 35 | } -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | import { OptionsWithUrl } from "cloudscraper"; 2 | 3 | export const AnimeGenres = [ 4 | "Acción", "Artes Marciales", "Aventuras", "Carreras", "Ciencia Ficción", "Comedia", "Demencia", "Demonios", "Deportes", "Drama", "Ecchi", "Escolares", "Espacial", "Fantasía", "Harem", "Histórico", "Infantil", "Josei", "Juegos", "Magia", "Mecha", "Militar", "Misterio", "Música", "Parodia", "Policía", "Psicológico", "Recuentos de la vida", "Romance", "Samurai", "Seinen", "Shoujo", "Shounen", "Sobrenatural", "Superpoderes", "Suspenso", "Terror", "Vampiros", "Yaoi", "Yuri" 5 | ] as const; 6 | 7 | export const AnimeStatuses = ["En emision", "Finalizado", "Proximamente"] as const; 8 | export const AnimeTypes = ["OVA", "Anime", "Película", "Especial"] as const 9 | 10 | export const CloudscraperOptions: OptionsWithUrl = { 11 | headers: { 12 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', 13 | 'Cache-Control': 'private', 14 | 'Referer': 'https://www.google.com/search?q=animeflv', 15 | 'Connection': 'keep-alive', 16 | }, 17 | uri: "" 18 | } 19 | 20 | export const FilterOrderEnum = { 21 | "Por Defecto": "default", 22 | "Recientemente Actualizados": "recent", 23 | "Recientemente Agregados": "added", 24 | "Nombre A-Z": "title", 25 | "Calificacion": "rating" 26 | } 27 | 28 | export const AnimeTypeEnum = { 29 | "Anime": "tv", 30 | "Película": "movie", 31 | "Especial": "special", 32 | "OVA": "ova" 33 | } 34 | 35 | export const AnimeStatusEnum = { 36 | "En emision": 1, 37 | "Finalizado": 2, 38 | "Proximamente": 3, 39 | } -------------------------------------------------------------------------------- /src/functions/getAnimeInfo.ts: -------------------------------------------------------------------------------- 1 | import { CloudscraperOptions } from "../constants"; 2 | import { load } from "cheerio"; 3 | import cloudscraper from "cloudscraper"; 4 | import type { AnimeData, AnimeGenre, AnimeStatus, AnimeType } from "../types"; 5 | 6 | export async function getAnimeInfo(animeId: string): Promise { 7 | 8 | if (!animeId || (typeof animeId !== "string")) { 9 | throw new TypeError(`El parámetro animeId debe ser una string no vacía, pasaste: ${animeId}`, { cause: `animeId: ${animeId}` }); 10 | } 11 | 12 | try { 13 | 14 | CloudscraperOptions.uri = 'https://www3.animeflv.net/anime/' + animeId; 15 | 16 | const animeData = (await cloudscraper(CloudscraperOptions)) as string; 17 | const $ = load(animeData); 18 | 19 | const animeInfo: AnimeData = { 20 | title: $('body > div.Wrapper > div > div > div.Ficha.fchlt > div.Container > h1').text(), 21 | alternative_titles: [], 22 | status: $('body > div.Wrapper > div > div > div.Container > div > aside > p > span').text() as AnimeStatus, 23 | rating: $('#votes_prmd').text(), 24 | type: $('body > div.Wrapper > div > div > div.Ficha.fchlt > div.Container > span').text() as AnimeType, 25 | cover: 'https://animeflv.net' + ($('body > div.Wrapper > div > div > div.Container > div > aside > div.AnimeCover > div > figure > img').attr('src') as string), 26 | synopsis: $('body > div.Wrapper > div > div > div.Container > div > main > section:nth-child(1) > div.Description > p').text(), 27 | genres: $('body > div.Wrapper > div > div > div.Container > div > main > section:nth-child(1) > nav > a').text().split(/(?=[A-Z])/) as AnimeGenre[], 28 | episodes: [], 29 | url: CloudscraperOptions.uri 30 | }; 31 | 32 | for (let i = 1; i <= JSON.parse($('script').eq(15).text().match(/episodes = (\[\[.*\].*])/)?.[1] as string).length; i++) { 33 | animeInfo.episodes.push({ 34 | number: i, 35 | url: 'https://www3.animeflv.net/ver/' + animeId + '-' + i 36 | }); 37 | }; 38 | 39 | $('body > div.Wrapper > div > div > div.Ficha.fchlt > div.Container > div:nth-child(3) > span').each((i, el) => { 40 | animeInfo.alternative_titles.push($(el).text()); 41 | }); 42 | 43 | return animeInfo; 44 | } catch { 45 | return null 46 | } 47 | } -------------------------------------------------------------------------------- /src/functions/searchAnimesByFilter.ts: -------------------------------------------------------------------------------- 1 | import cloudscraper from "cloudscraper" 2 | import { AnimeGenres, AnimeStatusEnum, AnimeTypeEnum, CloudscraperOptions, FilterOrderEnum } from "../constants" 3 | import { AnimeStatus, AnimeType, FilterOptions, FilterAnimeResults } from "../types" 4 | import { executeSearch } from "../utils" 5 | 6 | function generateRequestUrl(options?: FilterOptions): string { 7 | const quitarAcentos = (cadena: string) => { 8 | const acentos = { 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u', 'Á': 'A', 'É': 'E', 'Í': 'I', 'Ó': 'O', 'Ú': 'U' }; 9 | //@ts-ignore 10 | return cadena.split('').map(letra => acentos[letra] || letra).join('').toString(); 11 | } 12 | 13 | if (!options) return "https://www3.animeflv.net/browse?order=default" 14 | 15 | const FinalUrl = new URL("https://www3.animeflv.net/browse") 16 | 17 | let filteredGenres: string[] | string = "" 18 | let parsedStatuses: string[] | string = "" 19 | let parsedTypes: string[] | string = "" 20 | 21 | const genrePrefix = "genre[]" 22 | const typePrefix = "type[]" 23 | const statusPrefix = "status[]" 24 | const orderPrefix = "order" 25 | 26 | if (options.genres && Array.isArray(options.genres)) { 27 | filteredGenres = options.genres.filter(genre => AnimeGenres.includes(genre)) 28 | 29 | for (const genre of filteredGenres) { 30 | const normalizedGenre = quitarAcentos(genre).replace(/\s+/g, "-").toLowerCase() 31 | 32 | FinalUrl.searchParams.append(genrePrefix, normalizedGenre) 33 | } 34 | } 35 | 36 | if (options.statuses && Array.isArray(options.statuses)) { 37 | parsedStatuses = options.statuses.filter(status => status in AnimeStatusEnum) 38 | 39 | for (const status of parsedStatuses) { 40 | FinalUrl.searchParams.append(statusPrefix, AnimeStatusEnum[status as AnimeStatus].toString()) 41 | } 42 | } 43 | 44 | if (options.types && Array.isArray(options.types)) { 45 | parsedTypes = options.types.filter(type => type in AnimeTypeEnum) 46 | 47 | for (const type of parsedTypes) { 48 | FinalUrl.searchParams.append(typePrefix, AnimeTypeEnum[type as AnimeType]) 49 | } 50 | } 51 | 52 | if (options.order && (options.order in FilterOrderEnum)) { 53 | FinalUrl.searchParams.append(orderPrefix, FilterOrderEnum[options.order]) 54 | } else { 55 | FinalUrl.searchParams.append(orderPrefix, FilterOrderEnum["Por Defecto"]) 56 | } 57 | 58 | return FinalUrl.toString() 59 | } 60 | 61 | export async function searchAnimesByFilter(opts?: FilterOptions): Promise { 62 | try { 63 | /** La url del request con los filtros ya puestos */ 64 | const formatedUrl = generateRequestUrl(opts) 65 | 66 | CloudscraperOptions.uri = formatedUrl; 67 | 68 | const filterData = (await cloudscraper(CloudscraperOptions)) as string; 69 | 70 | return executeSearch(filterData) 71 | } 72 | 73 | catch { 74 | return null; 75 | } 76 | } -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { AnimeGenres, AnimeStatuses, AnimeTypes, FilterOrderEnum } from "../constants"; 2 | 3 | export type AnimeStatus = typeof AnimeStatuses[number]; 4 | export type AnimeType = typeof AnimeTypes[number]; 5 | export type AnimeGenre = typeof AnimeGenres[number]; 6 | export type FilterOrderType = keyof typeof FilterOrderEnum; 7 | export type FilterAnimeResults = SearchAnimeResults 8 | 9 | export interface PartialAnimeData { 10 | /** Título del animé */ 11 | title: string 12 | /** URL de la carátula del animé */ 13 | cover: string 14 | /** La sinopsis (descripción) del animé */ 15 | synopsis: string 16 | /** Evaluación de estrellas del animé */ 17 | rating: string 18 | /** Id del animé */ 19 | id: string 20 | /** El tipo de anime: OVA | Anime | Película | Especial */ 21 | type: AnimeType 22 | /** La URL directa a la página de éste animé */ 23 | url: string 24 | } 25 | 26 | export interface SearchAnimeResults { 27 | 28 | /** URL a la página anterior, o null en caso de no haber*/ 29 | previousPage: string | null 30 | /** URL a la página siguiente, o null en caso de no haber*/ 31 | nextPage: string | null 32 | /** Número de páginas con resultados de la búsqueda realizada */ 33 | foundPages: number 34 | /** Los animés encontrados en la búsqueda */ 35 | data: PartialAnimeData[] 36 | } 37 | 38 | export interface AnimeData { 39 | /** Titulo del animé */ 40 | title: string 41 | /** Array con titulos alternativos de este animé */ 42 | alternative_titles: string[] 43 | /** Estado de este animé: "En emision" | "Finalizado" | "Proximamente" */ 44 | status: AnimeStatus 45 | /** Evaluación de estrellas de este animé */ 46 | rating: string 47 | /** El tipo de anime: "OVA" | "Anime" | "Película" | "Especial" */ 48 | type: AnimeType 49 | /** URL a la carátula de este animé */ 50 | cover: string 51 | /** Sinopsis o descripción del animé */ 52 | synopsis: string 53 | /** Array con los géneros (etiquetas) del anime */ 54 | genres: AnimeGenre[] 55 | /** Número de episodios que tiene este animé */ 56 | episodes: EpisodeData[] 57 | /** La URL directa a la pagina del animé */ 58 | url: string 59 | } 60 | 61 | export interface EpisodeData { 62 | /** Número del episodio */ 63 | number: number 64 | /** Link del episodio */ 65 | url: string 66 | } 67 | 68 | export interface ChapterData { 69 | /** Título del episodio */ 70 | title: string 71 | /** Número del episodio */ 72 | chapter: number 73 | /** URL del thumbnail de este episodio */ 74 | cover: string 75 | /** URL directa del episodio */ 76 | url: string 77 | } 78 | 79 | export interface AnimeOnAirData { 80 | /** Título del animé */ 81 | title: string 82 | /** El tipo de anime: "OVA" | "Anime" | "Película" | "Especial" */ 83 | type: AnimeType 84 | /** La id de este animé */ 85 | id: string 86 | /** La URL directa a la página de este anime */ 87 | url: string 88 | } 89 | export interface FilterOptions { 90 | /** Lista de generos para la búsqueda */ 91 | genres?: AnimeGenre[] 92 | /** Lista de tipos para la búsqueda */ 93 | types?: AnimeType[] 94 | /** Los statuses de los animés para filtrar */ 95 | statuses?: AnimeStatus[] 96 | /** El orden en el que se recibirán los animés */ 97 | order?: FilterOrderType 98 | } 99 | -------------------------------------------------------------------------------- /test/test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { expect } from 'chai'; 4 | import { getAnimeInfo, searchAnime } from '../src/index'; 5 | 6 | describe('SCRAPPER SELECTORS', function () { 7 | it('La función getAnimeInfo debe devolver un objeto', async () => { 8 | const anime = await getAnimeInfo("one-piece-tv"); 9 | expect(anime).to.be.a('Object'); 10 | }); 11 | 12 | it('La función searchAnime debe devolver un objeto', async () => { 13 | const search = await searchAnime("One Piece"); 14 | expect(search).to.be.a('Object'); 15 | }); 16 | 17 | it('Si existe un error en getAnimeInfo, debe devolver null', async () => { 18 | const anime = await getAnimeInfo("one-piece"); 19 | expect(anime).to.eql(null); 20 | }); 21 | 22 | it('Si existe un error en searchAnime, debe devolver null', async () => { 23 | const search = await searchAnime("ぼっち・ざ・ろっ"); 24 | expect(search).to.eql(null); 25 | }); 26 | 27 | it('getAnimeInfo("sword-art-online") debe devolver información acerca del anime "Sword Art Online"', async () => { 28 | const anime = await getAnimeInfo("sword-art-online"); 29 | expect(anime).to.eql({ 30 | title: 'Sword Art Online', 31 | alternative_titles: ['ソードアート・オンライン'], 32 | status: 'Finalizado', 33 | rating: '4.5', 34 | type: 'Anime', 35 | cover: 'https://animeflv.net/uploads/animes/covers/825.jpg', 36 | synopsis: `Escapar es imposible hasta terminar el juego; un game over significaría una verdadera "muerte". Sin saber la "verdad" de la siguiente generación del Multijugador Masivo Online, 'Sword Art Online(SAO)', con 10 mil usuarios unidos juntos abriendo las cortinas para esta cruel batalla a muerte. Participando solo en SAO, el protagonista Kirito ha aceptado inmediatamente la "verdad" de este MMO.\n` + 37 | '

\n' + 38 | "Y, en el mundo del juego, un gigante castillo flotante llamado 'Aincrad', Kirito se distinguió a si mismo como un jugador solitario. Apuntando a terminar el juego al alcanzar la planta mas alta el solo continua avanzando arriesgadamente hasta que recibe una invitación a la fuerza de una guerrera y esgrimista experta, Asuna, con la cual tendra que hacer equipo.", 39 | genres: ['Acción', 'Aventuras', 'Fantasía', 'Juegos', 'Romance'], 40 | episodes: 25, 41 | url: 'https://www3.animeflv.net/anime/sword-art-online' 42 | }); 43 | }); 44 | 45 | it('searchAnime("High School of The Dead") debe devolver un objeto con DOS objetos dentro.', async () => { 46 | const search = await searchAnime("High School of The Dead"); 47 | expect(search).to.eql( 48 | { 49 | previousPage: null, 50 | nextPage: null, 51 | foundPages: 1, 52 | data: [ 53 | { 54 | title: 'Highschool of the Dead', 55 | cover: 'https://animeflv.net/uploads/animes/covers/4.jpg', 56 | synopsis: 'El mundo entero esta siendo dominado por una enfermedad mortal, esto convierte a los humanos en zombies. En Japón, muchos estudiantes de la preparatoria Fujimi, y la enfermera de la escuela, estaran juntos trantando de sobrevivir al presente Apocalipsis. La historia se centra en Takashi Komuro, uno de los estudiantes que ha sobrevivido a est...', 57 | rating: '4.7', 58 | id: 'highschool-of-the-dead', 59 | type: 'Anime', 60 | url: 'https://www3.animeflv.net/anime/highschool-of-the-dead' 61 | }, 62 | { 63 | title: 'Highschool of the Dead Ova', 64 | cover: 'https://animeflv.net/uploads/animes/covers/402.jpg', 65 | synopsis: 'Excelente anime que mezcla imagenes increibles de mujeres hermosas y un mundo invadido por zombies. Un grupo de estudiantes tratan de sobrevivir y libran grandes batallas cuerpo a cuerpo y con armas de primer nivel.', 66 | rating: '4.7', 67 | id: 'highschool-of-the-dead-ovas', 68 | type: 'OVA', 69 | url: 'https://www3.animeflv.net/anime/highschool-of-the-dead-ovas' 70 | } 71 | ] 72 | } 73 | ); 74 | }) 75 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AnimeFLV SCRAPPER ![Licence](https://img.shields.io/npm/l/animeflv-api) ![Version](https://img.shields.io/npm/v/animeflv-api) ![Known Vulnerabilities](https://snyk.io/test/github/mixdevcode/animeflv-api/badge.svg) ![Node Minimum Version](https://img.shields.io/badge/node-%3E%3D16.0.0-informational) 2 | ============ 3 | [![NPM](https://nodei.co/npm/animeflv-api.png)](https://nodei.co/npm/animeflv-api/) 4 | 5 | Librería Node.js para obtener información del sitio `https://www3.animeflv.net/` utilizando el método de Web-Scraping. 6 | 7 | Instalación 8 | ============ 9 | ```sh 10 | npm install animeflv-api 11 | ``` 12 | 13 | Uso 14 | ============ 15 | Una vez el paquete está instalado, puedes importar la librería utilizando "require": 16 | 17 | ```js 18 | const animeflv = require('animeflv-api'); 19 | ``` 20 | 21 | o utilizando "import": 22 | 23 | ```js 24 | import * as animeflv from 'animeflv-api'; 25 | ``` 26 | 27 | ## Funciones 28 | 29 | > **Note** Si quieres saber más acerca de los tipos, constantes y funciones puedes visitar la [Wiki](https://github.com/MixDevCode/animeflv-api/wiki), aquí solo se listarán ejemplos de uso. 30 | 31 | ### searchAnime(params) 32 | |Params|Type|Required| 33 | |-|-|:-:| 34 | |`query`|string|✅| 35 | 36 | ```js 37 | import { searchAnime } from 'animeflv-api'; 38 | 39 | searchAnime("Overlord").then((result) => { 40 | console.log(result); 41 | }); 42 | ``` 43 | 44 | ###### Respuesta 45 | 46 | Un objeto de tipo Promise<[SearchAnimeResults](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#searchanimeresults) | `null`> que contiene todos los animes encontrados utilizando el `query` especificado. 47 | 48 | ```js 49 | { 50 | previousPage: null, 51 | nextPage: null, 52 | foundPages: 1, 53 | data: [ 54 | { 55 | title: 'Overlord II', 56 | cover: 'https://animeflv.net/uploads/animes/covers/2856.jpg', 57 | synopsis: 'Segunda temporada de Overlord.', 58 | rating: '4.7', 59 | id: 'overlord-ii', 60 | type: 'Anime', 61 | url: 'https://www3.animeflv.net/anime/overlord-ii' 62 | } 63 | ... 64 | ] 65 | } 66 | ``` 67 | 68 | ### getAnimeInfo(params) 69 | 70 | |Params|Type|Required| 71 | |-|-|:-:| 72 | |`animeId`|string|✅| 73 | 74 | > **Note** el animeId es obtenido a través de la función `searchAnime` o removiendo `https://www3.animeflv.net/anime/` de la URL de un anime. 75 | ```js 76 | import { getAnimeInfo } from 'animeflv-api'; 77 | 78 | getAnimeInfo("one-piece-tv").then((result) => { 79 | console.log(result); 80 | }); 81 | ``` 82 | 83 | ###### Respuesta 84 | 85 | Un objeto de tipo Promise<[AnimeData](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#animedata) | `null`> que contiene la información del anime solicitado con el `animeId` especificado. 86 | 87 | ```js 88 | { 89 | title: 'One Piece', 90 | alternative_titles: [ 'ワンピース' ], 91 | status: 'En emision', 92 | rating: '4.6', 93 | type: 'Anime', 94 | cover: 'https://animeflv.net/uploads/animes/covers/7.jpg', 95 | synopsis: 'Una historia épica de piratas, donde narra...', 96 | genres: [ 97 | 'Acción', 98 | 'Aventuras', 99 | 'Comedia', 100 | 'Drama', 101 | 'Fantasía', 102 | 'Shounen', 103 | 'Superpoderes' 104 | ], 105 | episodes: 1047, 106 | url: 'https://www3.animeflv.net/anime/one-piece-tv' 107 | } 108 | ``` 109 | 110 | ### getLatest() 111 | 112 | ```js 113 | import { getLatest } from 'animeflv-api'; 114 | 115 | getLatest().then((result) => { 116 | console.log(result); 117 | }); 118 | ``` 119 | 120 | ###### Respuesta 121 | 122 | Un arreglo de tipo Promise<[ChapterData[ ]](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#chapterdata)> que contiene los últimos capítulos subidos al sitio web. 123 | 124 | ```js 125 | [ 126 | { 127 | title: 'Majutsushi Orphen Hagure Tabi: Urbanrama-hen', 128 | chapter: 1, 129 | cover: 'https://animeflv.net/uploads/animes/thumbs/3763.jpg', 130 | url: 'https://www3.animeflv.net/ver/majutsushi-orphen-hagure-tabi-urbanramahen-1' 131 | } 132 | ... 133 | ] 134 | ``` 135 | 136 | ### getOnAir() 137 | 138 | ```js 139 | import { getOnAir } from 'animeflv-api'; 140 | 141 | getOnAir().then((result) => { 142 | console.log(result); 143 | }); 144 | ``` 145 | 146 | ###### Respuesta 147 | 148 | Un arreglo de tipo Promise<[AnimeOnAirData[ ]](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#animeonairdata)> con todos los animes en emisión del sitio. 149 | 150 | ```js 151 | [ 152 | { 153 | title: 'One Piece Anime', 154 | type: 'Anime', 155 | id: 'one-piece-tv', 156 | url: 'https://www3.animeflv.net/anime/one-piece-tv' 157 | } 158 | ... 159 | ] 160 | ``` 161 | 162 | ### searchAnimesByFilter(params) 163 | 164 | |Params|Type|Required| 165 | |-|-|:-:| 166 | |`opts`|[FilterOptions](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#filteroptions)|❌| 167 | 168 | > **Note** Vease el ejemplo para entender el parámetro requerido por la función. 169 | 170 | ```js 171 | import { searchAnimesByFilter } from 'animeflv-api'; 172 | 173 | searchAnimesByFilter({ 174 | types: ["Anime"], 175 | genres: ["Acción", "Magia"], 176 | statuses: ["Finalizado"] 177 | }).then((result) => { 178 | console.log(result); 179 | }) 180 | ``` 181 | 182 | ###### Respuesta 183 | 184 | Un objeto Promise<[FilterAnimeResults](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#filteranimeresults) | `null`> con los resultados encontrados de los filtros definidos. 185 | 186 | ```js 187 | { 188 | previousPage: null, 189 | nextPage: 'https://www3.animeflv.net/browse?genre%5B%5D=accion&genre%5B%5D=magia&status%5B%5D=2&type%5B%5D=tv&order=default&page=2', 190 | foundPages: 44, 191 | data: [ 192 | { 193 | title: 'Arknights: Reimei Zensou', 194 | cover: 'https://animeflv.net/uploads/animes/covers/3712.jpg', 195 | synopsis: 'En la tierra de Terra, los desastres inexplicables están ocurriendo irregularmente en varios lugares. La mayoría de las personas allí viven en ciudades móviles que se han desarrollado durante un largo período de tiempo para escapar de los desastres.\n' + 196 | 'Las piedras preciosas con una enorme energía que quedaron en la tierra después del desastr...', 197 | rating: '4.5', 198 | id: 'arknights-reimei-zensou', 199 | type: 'Anime', 200 | url: 'https://www3.animeflv.net/anime/arknights-reimei-zensou' 201 | } 202 | ... 203 | ] 204 | } 205 | ``` 206 | 207 | ### searchAnimesBySpecificURL(params) 208 | 209 | |Params|Type|Required| 210 | |-|-|:-:| 211 | |`url`|string|✅| 212 | 213 | ```js 214 | import { searchAnimesBySpecificURL } from 'animeflv-api'; 215 | 216 | searchAnimesBySpecificURL("https://www3.animeflv.net/browse?q=dragon+ball&page=2").then((result) => { 217 | console.log(result); 218 | }) 219 | ``` 220 | 221 | ###### Respuesta 222 | 223 | Un objeto Promise<[SearchAnimeResults](https://github.com/MixDevCode/animeflv-api/wiki/Datatypes#searchanimeresults) | `null`> con los resultados encontrados de la `url` especificada. 224 | 225 | ```js 226 | { 227 | previousPage: 'https://www3.animeflv.net/browse?q=dragon+ball&page=1', 228 | nextPage: null, 229 | foundPages: 2, 230 | data: [ 231 | { 232 | title: 'Dragon Ball Z Pelicula 10: El regreso del Guerrero Legendario', 233 | cover: 'https://animeflv.net/uploads/animes/covers/1111.jpg', 234 | synopsis: 'Goten, Trunks y Videl se aventuran a ir en busca de las Esferas del Dragon...', 235 | rating: '4.2', 236 | id: 'dragon-ball-z-pelicula-10', 237 | type: 'Película', 238 | url: 'https://www3.animeflv.net/anime/dragon-ball-z-pelicula-10' 239 | } 240 | ... 241 | ] 242 | } 243 | ``` 244 | 245 | Disclaimer 246 | ============ 247 | El uso de animeflv-api es exclusivamente para fines educativos y de investigación. No nos hacemos responsables del uso indebido o ilegal de la misma, incluyendo pero no limitando a la recolección de datos sin el consentimiento del propietario del sitio web, violación de los términos de uso del sitio, o cualquier otra actividad ilegal. Es responsabilidad del usuario final cumplir con todas las leyes y regulaciones aplicables en su jurisdicción antes de utilizar la librería. 248 | 249 | Además, al utilizar esta librería, el usuario acepta que es consciente de las posibles consecuencias legales o técnicas que puedan surgir de su uso. Estas consecuencias incluyen, pero no se limitan a, el bloqueo de su dirección IP por parte del sitio web, la violación de los términos de uso del sitio, y cualquier otra acción tomada por el propietario del sitio web para proteger su contenido. 250 | 251 | Si eres el propietario del sitio web y deseas que cesemos el desarrollo de animeflv-api, te invitamos a contactarnos a través de soporte@mixdev.online. Haremos todo lo posible para cumplir con tu solicitud de manera rápida y eficiente. 252 | 253 | En resumen, el uso de animeflv-api es bajo su propio riesgo. 254 | 255 | ## TODO 256 | - [x] Convertir el módulo a TS 257 | - [x] Agregar una función para obtener los últimos capítulos subidos 258 | - [x] Agregar una función para obtener los animes en emisión 259 | - [x] Agregar una función para obtener los próximos animes 260 | - [x] Modificar las funciones en caso de existir una paginación en el sitio web. 261 | 262 | ## Contribuyentes 263 | 264 | 265 | 266 | 269 | 272 | 273 | 274 | 277 | 280 | 281 |
267 | Shompi 268 | 270 | MixDevCode 271 |
275 | Shompi 276 | 278 | MixDevCode 279 |
282 | --------------------------------------------------------------------------------