├── .gitignore ├── vercel.json ├── src ├── controllers │ ├── index.js │ ├── latestRelease.js │ ├── popularSeries.js │ ├── search.js │ ├── animeMovie.js │ ├── animeDetail.js │ └── episodeDetail.js ├── routes │ └── index.js ├── utils │ └── index.js └── index.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "src/index.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "src/index.js" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/controllers/index.js: -------------------------------------------------------------------------------- 1 | import { latestRelease } from "./latestRelease.js"; 2 | import { animeDetail } from "./animeDetail.js"; 3 | import { episodeDetail } from "./episodeDetail.js"; 4 | import { popularSeries } from "./popularSeries.js"; 5 | import { searchAnime } from "./search.js"; 6 | import { animeMovie } from "./animeMovie.js"; 7 | 8 | export { 9 | latestRelease, 10 | animeDetail, 11 | episodeDetail, 12 | popularSeries, 13 | searchAnime, 14 | animeMovie 15 | }; -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { animeDetail, latestRelease,episodeDetail, popularSeries, searchAnime, animeMovie } from "../controllers/index.js"; 3 | 4 | const router = Router(); 5 | 6 | router.get('/latest/', latestRelease); 7 | router.get('/latest/page/:page', latestRelease); 8 | router.get('/popular', popularSeries); 9 | router.get('/movie', animeMovie); 10 | router.get('/movie/page/:page', animeMovie); 11 | router.get('/anime/:slug', animeDetail); 12 | router.get('/episode/:slug', episodeDetail); 13 | router.get('/search/:query', searchAnime); 14 | 15 | export default router; 16 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | import { default as Axios } from "axios"; 2 | import { load } from "cheerio"; 3 | 4 | export const BASE_URL = 'https://anime-indo.biz'; 5 | 6 | export const requestFailed = (req,res,err)=>{ 7 | res.status(502).send({ 8 | 'status':false, 9 | 'message':err.message 10 | }) 11 | } 12 | 13 | export const getData = async (url) => { 14 | try { 15 | const response = await Axios.get(url); 16 | const $ = load(response.data); 17 | 18 | const streamUrl = $("video > source").attr("src"); 19 | 20 | return streamUrl; 21 | } catch (error) { 22 | return error.message; 23 | } 24 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kumanime-api", 3 | "version": "1.0.0", 4 | "description": "Kumanime API adalah rest api untuk streaming anime subtitle indo.", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon src/index.js" 10 | }, 11 | "keywords": [ 12 | "anime-api", 13 | "kumanime", 14 | "anime", 15 | "js-project" 16 | ], 17 | "author": "Mastay", 18 | "license": "ISC", 19 | "dependencies": { 20 | "axios": "^1.6.5", 21 | "cheerio": "^1.0.0-rc.12", 22 | "cors": "^2.8.5", 23 | "express": "^4.18.2" 24 | }, 25 | "devDependencies": { 26 | "nodemon": "^3.0.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import router from './routes/index.js'; 3 | import cors from 'cors'; 4 | import axios from 'axios'; 5 | 6 | const app = express(); 7 | const port = 3000; 8 | 9 | const corsOptions = { 10 | origin: '*', 11 | credentials: true 12 | } 13 | 14 | app.use(cors()); 15 | 16 | app.use('/api', router) 17 | app.use('/', (req, res) => { 18 | res.json({ 19 | author: "MastayY", 20 | routes: { 21 | latestAnime: "/latest", 22 | popularAnime: "/popular", 23 | movieAnime: "/movie/page/:page", 24 | search: "/search/:query", 25 | detailAnime: "/anime/:slug", 26 | detailEpisode: "/episode/:slug", 27 | }, 28 | }); 29 | }); 30 | 31 | app.use('*',(req,res) =>{ 32 | res.json({ 33 | 'status':'not found path', 34 | message: 'read the docs here https://github.com/MastayY/kumanime-api' 35 | }) 36 | }) 37 | app.listen(port, () => { 38 | console.log('listening on port', port) 39 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MastayY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/controllers/latestRelease.js: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { BASE_URL, requestFailed } from "../utils/index.js"; 3 | import { default as Axios } from "axios"; 4 | 5 | const headers = { 6 | Accept: "application/json, text/plain, */*", 7 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 8 | "Accept-Language": "en-US,en;q=0.9", 9 | "Accept-Encoding": "gzip, compress, deflate, br", 10 | } 11 | 12 | export const latestRelease = async (req, res) => { 13 | const params = req.params.page; 14 | const page = typeof params == "undefined" ? "1" : `${params}`; 15 | const url = `${BASE_URL}/page/${page}/`; 16 | 17 | let data = []; 18 | 19 | try{ 20 | const response = await Axios.get(url, { 21 | headers 22 | }); 23 | const $ = load(response.data); 24 | 25 | $("#content-wrap > div.ngiri > div.menu > a").each(function (i, el) { 26 | data.push({ 27 | title: $(el).find("div > p").text(), 28 | slug: this.attribs.href.match(/\/([^/]+)\/$/)[1], 29 | poster: $(el).find("div > img").attr("data-original"), 30 | episode: $(el).find("span.eps").text(), 31 | }); 32 | }); 33 | 34 | res.status(200).json({ 35 | status: "success", 36 | data, 37 | }); 38 | 39 | } catch(err) { 40 | requestFailed(req, res, err); 41 | } 42 | } -------------------------------------------------------------------------------- /src/controllers/popularSeries.js: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { default as Axios } from "axios"; 3 | import { BASE_URL, requestFailed } from "../utils/index.js"; 4 | 5 | const headers = { 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "Accept-Encoding": "gzip,deflate", 9 | } 10 | 11 | export const popularSeries = async (req, res) => { 12 | let data = []; 13 | const domainUrl = "https://anime-indo.biz"; 14 | 15 | try { 16 | const response = await Axios.get(BASE_URL, {headers}); 17 | const $ = load(response.data); 18 | 19 | $("#content-wrap > .nganan > .menu > table").each((i, el) => { 20 | data.push({ 21 | title: $(el).find("tbody > tr > .zvidesc > a").text(), 22 | poster: domainUrl + $(el).find("tbody > tr > .zvithumb > a > img").attr("src"), 23 | slug: $(el).find("tbody > tr > .zvidesc > a").attr("href").match(/\/([^/]+)\/$/)[1], 24 | genres: $(el) 25 | .find(" tbody > tr > td.zvidesc") 26 | .text() 27 | .match(/[A-Z][\w ]+(?=,|\s|$)/g) 28 | .slice(1), 29 | }); 30 | }); 31 | 32 | res.status(200).json({ 33 | status: "success", 34 | data, 35 | }); 36 | } catch (err) { 37 | requestFailed(req, res, err); 38 | } 39 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kumanime API 2 |
3 |
4 |
5 |
6 |
7 |
9 | Rest API gratis untuk mendapatkan data anime serta link streaming anime dari website AnimeIndo 10 |
11 | 12 | 13 | # Instalasi 14 | 15 | - Jalankan perintah di terminal 16 | 17 | ```sh 18 | # clone repo 19 | git clone https://github.com/MastayY/kumanime-api.git 20 | 21 | # masuk folder 22 | cd kumanime-api 23 | 24 | # install dependensi 25 | npm install 26 | 27 | # jalankan server 28 | npm run dev 29 | ``` 30 | 31 | - Server akan berjalan di http://localhost:3000 32 | 33 | # Routes 34 | Endpoint : http://localhost:3000/api 35 | 36 | | Endpoint | Params | Description | 37 | | --------------------- | --------------- | -------------------------- | 38 | | /latest | - | Latest Release | 39 | | /popular | - | Popular Series | 40 | | /movie/page/:page | :page | Anime Movie | 41 | | /search/:query | :query | Search Anime | 42 | | /anime/:slug | :slug | Anime details | 43 | | /episode/:slug | :slug | Detail Episode | 44 | 45 | # Support Me 46 | [Saweria](https://saweria.co/Mastay) 47 | -------------------------------------------------------------------------------- /src/controllers/search.js: -------------------------------------------------------------------------------- 1 | import { default as Axios } from "axios"; 2 | import { BASE_URL, requestFailed } from "../utils/index.js"; 3 | import { load } from "cheerio"; 4 | 5 | const headers = { 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "Accept-Encoding": "gzip,deflate" 9 | } 10 | 11 | export const searchAnime = async (req, res) => { 12 | const params = req.params.query; 13 | const url = `${BASE_URL}/search/${params}/`; 14 | 15 | 16 | try { 17 | const response = await Axios.get(url, {headers}); 18 | const $ = load(response.data); 19 | 20 | let data = []; 21 | 22 | $("#content-wrap > div.menu > table").each((i, el) => { 23 | data.push({ 24 | title: $(el).find(" tbody > tr > td.videsc > a").text(), 25 | poster: $(el).find("img").attr("data-original"), 26 | slug: $(el).find(" tbody > tr > td.vithumb > a").attr("href").match(/\/anime\/([^/]+)\//)?.[1], 27 | type: $(el).find(" tbody > tr > td.videsc > span:nth-child(3)").text(), 28 | synopsis: $(el).find(" tbody > tr > td.videsc > p").text(), 29 | release: $(el).find(" tbody > tr > td.videsc > span:nth-child(5)").text(), 30 | duration: $(el).find("tbody > tr > td.videsc > span:nth-child(4)").text(), 31 | }); 32 | }); 33 | 34 | res.status(200).json({ 35 | status: "success", 36 | query: params, 37 | data 38 | }); 39 | } catch (err) { 40 | requestFailed(req, res, err); 41 | } 42 | } -------------------------------------------------------------------------------- /src/controllers/animeMovie.js: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { default as Axios } from "axios"; 3 | import { BASE_URL, requestFailed } from "../utils/index.js"; 4 | 5 | const headers = { 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "Accept-Encoding": "gzip,deflate", 9 | } 10 | 11 | export const animeMovie = async (req, res) => { 12 | const params = req.params.page; 13 | const page = typeof params == "undefined" ? "1" : `${params}`; 14 | const url = `${BASE_URL}/movie/page/${page}/`; 15 | const domainUrl = "https://anime-indo.biz"; 16 | let data = []; 17 | 18 | try { 19 | const response = await Axios.get(url, {headers}); 20 | const $ = load(response.data); 21 | 22 | 23 | $("#content-wrap > div.menu > table").each((i, el) => { 24 | data.push({ 25 | title: $(el).find(" tbody > tr > td.videsc > a").text(), 26 | poster: domainUrl + $(el).find("img").attr("src"), 27 | slug: $(el).find(" tbody > tr > td.vithumb > a").attr("href").match(/\/anime\/([^/]+)\//)?.[1], 28 | type: $(el).find(" tbody > tr > td.videsc > span:nth-child(3)").text(), 29 | synopsis: $(el).find(" tbody > tr > td.videsc > p").text(), 30 | release: $(el).find(" tbody > tr > td.videsc > span:nth-child(5)").text(), 31 | duration: $(el).find("tbody > tr > td.videsc > span:nth-child(4)").text(), 32 | }); 33 | }); 34 | 35 | res.status(200).json({ 36 | status: "success", 37 | data, 38 | }); 39 | } catch (err) { 40 | requestFailed(req, res, err); 41 | } 42 | } -------------------------------------------------------------------------------- /src/controllers/animeDetail.js: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { default as Axios } from "axios"; 3 | import { BASE_URL, requestFailed } from "../utils/index.js"; 4 | 5 | const headers = { 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "Accept-Encoding": "gzip,deflate", 9 | } 10 | 11 | export const animeDetail = async (req, res) => { 12 | const slug = req.params.slug; 13 | const url = `${BASE_URL}/anime/${slug}/`; 14 | const domainUrl = "https://anime-indo.biz"; 15 | 16 | try { 17 | const response = await Axios.get(url, {headers}); 18 | const $ = load(response.data); 19 | const mainElement = $("div.detail"); 20 | 21 | let animeData = {}; 22 | 23 | animeData.title = mainElement.find("h2").text().replace(/^Nonton\s/, ''); 24 | animeData.poster = domainUrl + mainElement.find("img").attr("src"); 25 | animeData.synopsis = mainElement.find("p").text(); 26 | 27 | let genreList = []; 28 | mainElement.find("li").each((i, el) => { 29 | const genreTitle = $(el).find("a").text(); 30 | genreList.push(genreTitle); 31 | }); 32 | 33 | let epsList = []; 34 | $("#content-wrap > div.ngirix > div:nth-child(4) > div > a").each((i, el) => { 35 | epsList.push({ 36 | eps_title: `Episode${$(el).text()}`, 37 | eps_slug: $(el).attr("href").match(/\/([^/]+)\/$/)[1], 38 | }); 39 | } 40 | ); 41 | 42 | animeData.genres = genreList; 43 | animeData.episodes = epsList; 44 | 45 | res.status(200).json({ 46 | status: "success", 47 | animeData, 48 | }); 49 | 50 | } catch(err) { 51 | requestFailed(req, res, err); 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /src/controllers/episodeDetail.js: -------------------------------------------------------------------------------- 1 | import { load } from "cheerio"; 2 | import { BASE_URL, getData, requestFailed } from "../utils/index.js"; 3 | import { default as Axios } from "axios"; 4 | 5 | const headers = { 6 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3", 7 | "Accept-Language": "en-US,en;q=0.9", 8 | "Accept-Encoding": "gzip,deflate" 9 | } 10 | 11 | export const episodeDetail = async (req, res) => { 12 | const slug = req.params.slug; 13 | const url = `${BASE_URL}/${slug}/`; 14 | const domainUrl = "https://anime-indo.biz"; 15 | 16 | try { 17 | const response = await Axios.get(url, {headers}); 18 | const $ = load(response.data); 19 | const mainElement = $(".detail"); 20 | 21 | let eps_detail = {}; 22 | // const RawStreamUrl = domainUrl + $(".server:contains('B-TUBE')").attr("data-video"); 23 | // const streamUrl = await getData(RawStreamUrl); 24 | 25 | eps_detail.title = mainElement.find("strong").text(); 26 | eps_detail.poster = domainUrl + mainElement.find("img").attr("src"); 27 | 28 | const streamUrl = $(".server:nth-child(1)").attr("data-video") || $(".server:nth-child(2)").attr("data-video") || "-"; 29 | eps_detail.stream_url = streamUrl.startsWith("//gdrive") ? streamUrl : streamUrl.startsWith("https://") ? streamUrl : domainUrl + streamUrl; 30 | eps_detail.synopsis = mainElement.find("p").text(); 31 | eps_detail.anime_slug = $(".navi > a:contains('Semua')").attr("href")?.match(/\/anime\/([^/]+)\//)?.[1] || "-"; 32 | eps_detail.next_eps_slug = $(".navi > a:contains('Next')").attr("href")?.match(/\/([^\/]+)\/$/)?.[1] || "-"; 33 | eps_detail.prev_eps_slug = $(".navi > a:contains('Prev')").attr("href")?.match(/\/([^\/]+)/)?.[1] || "-"; 34 | 35 | res.status(200).json({ 36 | status: "success", 37 | eps_detail, 38 | }); 39 | } catch (e) { 40 | requestFailed(req, res, e); 41 | } 42 | } --------------------------------------------------------------------------------