From b18b4d23b483404266096a6eee90074f9360ba05 Mon Sep 17 00:00:00 2001 From: himanshu8443 Date: Sun, 27 Jul 2025 11:02:34 +0530 Subject: [PATCH] feat: add catalog, meta, posts, and stream modules for Animetsu provider --- dist/animetsu/catalog.js | 1 + dist/animetsu/meta.js | 1 + dist/animetsu/posts.js | 1 + dist/animetsu/stream.js | 1 + providers/animetsu/catalog.ts | 24 ++++++++ providers/animetsu/meta.ts | 109 ++++++++++++++++++++++++++++++++++ providers/animetsu/posts.ts | 90 ++++++++++++++++++++++++++++ providers/animetsu/stream.ts | 94 +++++++++++++++++++++++++++++ 8 files changed, 321 insertions(+) create mode 100644 dist/animetsu/catalog.js create mode 100644 dist/animetsu/meta.js create mode 100644 dist/animetsu/posts.js create mode 100644 dist/animetsu/stream.js create mode 100644 providers/animetsu/catalog.ts create mode 100644 providers/animetsu/meta.ts create mode 100644 providers/animetsu/posts.ts create mode 100644 providers/animetsu/stream.ts diff --git a/dist/animetsu/catalog.js b/dist/animetsu/catalog.js new file mode 100644 index 0000000..16d7e3f --- /dev/null +++ b/dist/animetsu/catalog.js @@ -0,0 +1 @@ +"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.genres=exports.catalog=void 0,exports.catalog=[{title:"Popular",filter:"/api/anime/search?query=&page=1&perPage=35&year=any&sort=favourites&season=any&format=any&status=any"},{title:"Trending",filter:"/api/anime/search?query=&page=1&perPage=35&year=any&sort=trending&season=any&format=any&status=any"},{title:"Top Rated",filter:"/api/anime/search?query=&page=1&perPage=35&year=any&sort=rating&season=any&format=any&status=any"},{title:"Recently Updated",filter:"/api/anime/search?query=&page=1&perPage=35&year=any&sort=updated&season=any&format=any&status=any"}],exports.genres=[]; \ No newline at end of file diff --git a/dist/animetsu/meta.js b/dist/animetsu/meta.js new file mode 100644 index 0000000..beacfc2 --- /dev/null +++ b/dist/animetsu/meta.js @@ -0,0 +1 @@ +"use strict";var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){return new(P||(P=Promise))(function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator.throw(value))}catch(e){reject(e)}}function step(result){var value;result.done?resolve(result.value):(value=result.value,value instanceof P?value:new P(function(resolve){resolve(value)})).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())})};Object.defineProperty(exports,"__esModule",{value:!0}),exports.getMeta=void 0;const getMeta=function(_a){return __awaiter(this,arguments,void 0,function*({link:link,providerContext:providerContext}){var _b,_c,_d,_e,_f,_g;try{const{axios:axios}=providerContext,baseUrl="https://backend.animetsu.to",url=`${baseUrl}/api/anime/info/${link}`,data=(yield axios.get(url,{headers:{Referer:"https://animetsu.to/"}})).data,meta={title:(null===(_b=data.title)||void 0===_b?void 0:_b.english)||(null===(_c=data.title)||void 0===_c?void 0:_c.romaji)||(null===(_d=data.title)||void 0===_d?void 0:_d.native)||"",synopsis:data.description||"",image:(null===(_e=data.coverImage)||void 0===_e?void 0:_e.extraLarge)||(null===(_f=data.coverImage)||void 0===_f?void 0:_f.large)||(null===(_g=data.coverImage)||void 0===_g?void 0:_g.medium)||"",tags:[null==data?void 0:data.format,null==data?void 0:data.status,...(null==data?void 0:data.genres)||[]].filter(Boolean),imdbId:"",type:"MOVIE"===data.format?"movie":"series"},linkList=[];try{const episodes=(yield axios.get(`${baseUrl}/api/anime/eps/${link}`,{headers:{Referer:"https://animetsu.to/"}})).data;if(episodes&&episodes.length>0){const directLinks=[];episodes.forEach(episode=>{const title=`Episode ${episode.number}`,episodeLink=`${link}:${episode.number}`;episodeLink&&title&&directLinks.push({title:title,link:episodeLink})}),linkList.push({title:meta.title,directLinks:directLinks})}else linkList.push({title:meta.title,directLinks:[{title:"Movie",link:`${link}:1`}]})}catch(episodeErr){linkList.push({title:meta.title,directLinks:[{title:meta.title,link:`${link}:1`}]})}return Object.assign(Object.assign({},meta),{linkList:linkList})}catch(err){return{title:"",synopsis:"",image:"",imdbId:"",type:"movie",linkList:[]}}})};exports.getMeta=getMeta; \ No newline at end of file diff --git a/dist/animetsu/posts.js b/dist/animetsu/posts.js new file mode 100644 index 0000000..acc4397 --- /dev/null +++ b/dist/animetsu/posts.js @@ -0,0 +1 @@ +"use strict";var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){return new(P||(P=Promise))(function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator.throw(value))}catch(e){reject(e)}}function step(result){var value;result.done?resolve(result.value):(value=result.value,value instanceof P?value:new P(function(resolve){resolve(value)})).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())})};Object.defineProperty(exports,"__esModule",{value:!0}),exports.getSearchPosts=exports.getPosts=void 0;const getPosts=function(_a){return __awaiter(this,arguments,void 0,function*({filter:filter,page:page,signal:signal,providerContext:providerContext}){const{axios:axios}=providerContext,url=new URL("https://backend.animetsu.to"+filter);return url.searchParams.set("page",page.toString()),posts({url:url.toString(),signal:signal,axios:axios})})};exports.getPosts=getPosts;const getSearchPosts=function(_a){return __awaiter(this,arguments,void 0,function*({searchQuery:searchQuery,page:page,signal:signal,providerContext:providerContext}){const{axios:axios}=providerContext;return posts({url:`https://backend.animetsu.to/api/anime/search?query=${encodeURIComponent(searchQuery)}&page=${page}&perPage=35&year=any&sort=favourites&season=any&format=any&status=any`,signal:signal,axios:axios})})};function posts(_a){return __awaiter(this,arguments,void 0,function*({url:url,signal:signal,axios:axios}){var _b;try{const data=null===(_b=(yield axios.get(url,{signal:signal,headers:{Referer:"https://animetsu.to/"}})).data)||void 0===_b?void 0:_b.results,catalog=[];return null==data||data.map(element=>{var _a,_b,_c,_d,_e,_f,_g;const title=(null===(_a=element.title)||void 0===_a?void 0:_a.english)||(null===(_b=element.title)||void 0===_b?void 0:_b.romaji)||(null===(_c=element.title)||void 0===_c?void 0:_c.native),link=null===(_d=element.id)||void 0===_d?void 0:_d.toString(),image=(null===(_e=element.coverImage)||void 0===_e?void 0:_e.large)||(null===(_f=element.coverImage)||void 0===_f?void 0:_f.extraLarge)||(null===(_g=element.coverImage)||void 0===_g?void 0:_g.medium);title&&link&&image&&catalog.push({title:title,link:link,image:image})}),catalog}catch(err){return[]}})}exports.getSearchPosts=getSearchPosts; \ No newline at end of file diff --git a/dist/animetsu/stream.js b/dist/animetsu/stream.js new file mode 100644 index 0000000..f616573 --- /dev/null +++ b/dist/animetsu/stream.js @@ -0,0 +1 @@ +"use strict";var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){return new(P||(P=Promise))(function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator.throw(value))}catch(e){reject(e)}}function step(result){var value;result.done?resolve(result.value):(value=result.value,value instanceof P?value:new P(function(resolve){resolve(value)})).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())})};Object.defineProperty(exports,"__esModule",{value:!0}),exports.getStream=void 0;const getStream=function(_a){return __awaiter(this,arguments,void 0,function*({link:id,providerContext:providerContext}){try{const{axios:axios}=providerContext,baseUrl="https://backend.animetsu.to",[animeId,episodeNumber]=id.split(":");if(!animeId||!episodeNumber)throw new Error("Invalid link format");const servers=["pahe","zoro"],streamLinks=[];return yield Promise.all(servers.map(server=>__awaiter(this,void 0,void 0,function*(){try{const url=`${baseUrl}/api/anime/tiddies?server=${server}&id=${animeId}&num=${episodeNumber}&subType=sub`,res=yield axios.get(url,{headers:{Referer:"https://animetsu.to/"}});res.data&&res.data.sources&&res.data.sources.forEach(source=>{streamLinks.push({server:server,link:source.url,type:source.url.includes(".m3u8")?"m3u8":"mp4",quality:source.quality,headers:{Referer:"https://animetsu.to/",Origin:"https://animetsu.to"},subtitles:[]})})}catch(e){}}))),yield Promise.all(servers.map(server=>__awaiter(this,void 0,void 0,function*(){try{const url=`${baseUrl}/api/anime/tiddies?server=${server}&id=${animeId}&num=${episodeNumber}&subType=dub`,res=yield axios.get(url,{headers:{Referer:"https://animetsu.to/"}});res.data&&res.data.sources&&res.data.sources.forEach(source=>{streamLinks.push({server:`${server} (Dub)`,link:source.url,type:source.url.includes(".m3u8")?"m3u8":"mp4",quality:source.quality,headers:{Referer:"https://animetsu.to/",Origin:"https://animetsu.to"},subtitles:[]})})}catch(e){}}))),streamLinks}catch(err){return[]}})};exports.getStream=getStream; \ No newline at end of file diff --git a/providers/animetsu/catalog.ts b/providers/animetsu/catalog.ts new file mode 100644 index 0000000..498c8df --- /dev/null +++ b/providers/animetsu/catalog.ts @@ -0,0 +1,24 @@ +export const catalog = [ + { + title: "Popular", + filter: + "/api/anime/search?query=&page=1&perPage=35&year=any&sort=favourites&season=any&format=any&status=any", + }, + { + title: "Trending", + filter: + "/api/anime/search?query=&page=1&perPage=35&year=any&sort=trending&season=any&format=any&status=any", + }, + { + title: "Top Rated", + filter: + "/api/anime/search?query=&page=1&perPage=35&year=any&sort=rating&season=any&format=any&status=any", + }, + { + title: "Recently Updated", + filter: + "/api/anime/search?query=&page=1&perPage=35&year=any&sort=updated&season=any&format=any&status=any", + }, +]; + +export const genres = []; diff --git a/providers/animetsu/meta.ts b/providers/animetsu/meta.ts new file mode 100644 index 0000000..a25b587 --- /dev/null +++ b/providers/animetsu/meta.ts @@ -0,0 +1,109 @@ +import { Info, Link, ProviderContext } from "../types"; + +export const getMeta = async function ({ + link, + providerContext, +}: { + link: string; + providerContext: ProviderContext; +}): Promise { + try { + const { axios } = providerContext; + const baseUrl = "https://backend.animetsu.to"; + const url = `${baseUrl}/api/anime/info/${link}`; + + const res = await axios.get(url, { + headers: { + Referer: "https://animetsu.to/", + }, + }); + const data = res.data; + + const meta = { + title: + data.title?.english || data.title?.romaji || data.title?.native || "", + synopsis: data.description || "", + image: + data.coverImage?.extraLarge || + data.coverImage?.large || + data.coverImage?.medium || + "", + tags: [data?.format, data?.status, ...(data?.genres || [])].filter( + Boolean + ), + imdbId: "", + type: data.format === "MOVIE" ? "movie" : "series", + }; + + const linkList: Link[] = []; + + // Get episodes data + try { + const episodesRes = await axios.get(`${baseUrl}/api/anime/eps/${link}`, { + headers: { + Referer: "https://animetsu.to/", + }, + }); + const episodes = episodesRes.data; + + if (episodes && episodes.length > 0) { + const directLinks: Link["directLinks"] = []; + + episodes.forEach((episode: any) => { + const title = `Episode ${episode.number}`; + const episodeLink = `${link}:${episode.number}`; + + if (episodeLink && title) { + directLinks.push({ + title, + link: episodeLink, + }); + } + }); + + linkList.push({ + title: meta.title, + directLinks: directLinks, + }); + } else { + // Movie case - single episode + linkList.push({ + title: meta.title, + directLinks: [ + { + title: "Movie", + link: `${link}:1`, + }, + ], + }); + } + } catch (episodeErr) { + console.error("Error fetching episodes:", episodeErr); + // Fallback for movie or single episode + linkList.push({ + title: meta.title, + directLinks: [ + { + title: meta.title, + link: `${link}:1`, + }, + ], + }); + } + + return { + ...meta, + linkList: linkList, + }; + } catch (err) { + console.error("animetsu meta error:", err); + return { + title: "", + synopsis: "", + image: "", + imdbId: "", + type: "movie", + linkList: [], + }; + } +}; diff --git a/providers/animetsu/posts.ts b/providers/animetsu/posts.ts new file mode 100644 index 0000000..8911ce6 --- /dev/null +++ b/providers/animetsu/posts.ts @@ -0,0 +1,90 @@ +import { Post, ProviderContext } from "../types"; + +export const getPosts = async function ({ + filter, + page, + signal, + providerContext, +}: { + filter: string; + page: number; + providerValue: string; + signal: AbortSignal; + providerContext: ProviderContext; +}): Promise { + const { axios } = providerContext; + const baseUrl = "https://backend.animetsu.to"; + + // Parse filter to modify page parameter + const url = new URL(baseUrl + filter); + url.searchParams.set("page", page.toString()); + + return posts({ url: url.toString(), signal, axios }); +}; + +export const getSearchPosts = async function ({ + searchQuery, + page, + signal, + providerContext, +}: { + searchQuery: string; + page: number; + providerValue: string; + signal: AbortSignal; + providerContext: ProviderContext; +}): Promise { + const { axios } = providerContext; + const baseUrl = "https://backend.animetsu.to"; + const url = `${baseUrl}/api/anime/search?query=${encodeURIComponent( + searchQuery + )}&page=${page}&perPage=35&year=any&sort=favourites&season=any&format=any&status=any`; + + return posts({ url, signal, axios }); +}; + +async function posts({ + url, + signal, + axios, +}: { + url: string; + signal: AbortSignal; + axios: ProviderContext["axios"]; +}): Promise { + try { + const res = await axios.get(url, { + signal, + headers: { + Referer: "https://animetsu.to/", + }, + }); + const data = res.data?.results; + const catalog: Post[] = []; + + data?.map((element: any) => { + const title = + element.title?.english || + element.title?.romaji || + element.title?.native; + const link = element.id?.toString(); + const image = + element.coverImage?.large || + element.coverImage?.extraLarge || + element.coverImage?.medium; + + if (title && link && image) { + catalog.push({ + title: title, + link: link, + image: image, + }); + } + }); + + return catalog; + } catch (err) { + console.error("animetsu error ", err); + return []; + } +} diff --git a/providers/animetsu/stream.ts b/providers/animetsu/stream.ts new file mode 100644 index 0000000..d433cb0 --- /dev/null +++ b/providers/animetsu/stream.ts @@ -0,0 +1,94 @@ +import { Stream, ProviderContext } from "../types"; + +export const getStream = async function ({ + link: id, + providerContext, +}: { + link: string; + providerContext: ProviderContext; +}): Promise { + try { + const { axios } = providerContext; + const baseUrl = "https://backend.animetsu.to"; + + // Parse link format: "animeId:episodeNumber" + const [animeId, episodeNumber] = id.split(":"); + + if (!animeId || !episodeNumber) { + throw new Error("Invalid link format"); + } + + const servers = ["pahe", "zoro"]; // Available servers based on API structure + const streamLinks: Stream[] = []; + + await Promise.all( + servers.map(async (server) => { + try { + const url = `${baseUrl}/api/anime/tiddies?server=${server}&id=${animeId}&num=${episodeNumber}&subType=sub`; + + const res = await axios.get(url, { + headers: { + Referer: "https://animetsu.to/", + }, + }); + + if (res.data && res.data.sources) { + res.data.sources.forEach((source: any) => { + streamLinks.push({ + server: server, + link: source.url, + type: source.url.includes(".m3u8") ? "m3u8" : "mp4", + quality: source.quality, + headers: { + Referer: "https://animetsu.to/", + Origin: "https://animetsu.to", + }, + subtitles: [], // No subtitle info provided in API response + }); + }); + } + } catch (e) { + console.log(`Error with server ${server}:`, e); + } + }) + ); + + // Try dub version as well + await Promise.all( + servers.map(async (server) => { + try { + const url = `${baseUrl}/api/anime/tiddies?server=${server}&id=${animeId}&num=${episodeNumber}&subType=dub`; + + const res = await axios.get(url, { + headers: { + Referer: "https://animetsu.to/", + }, + }); + + if (res.data && res.data.sources) { + res.data.sources.forEach((source: any) => { + streamLinks.push({ + server: `${server} (Dub)`, + link: source.url, + type: source.url.includes(".m3u8") ? "m3u8" : "mp4", + quality: source.quality, + headers: { + Referer: "https://animetsu.to/", + Origin: "https://animetsu.to", + }, + subtitles: [], + }); + }); + } + } catch (e) { + console.log(`Error with server ${server} (dub):`, e); + } + }) + ); + + return streamLinks; + } catch (err) { + console.error("animetsu stream error:", err); + return []; + } +};