Create a declaration file online-streaming-provider.d.ts
.
// online-streaming-provider.d.ts
declare type SearchResult = {
id: string // Passed to findEpisode
title: string
url: string
subOrDub: SubOrDub
}
declare type SubOrDub = "sub" | "dub" | "both"
// Passed to findEpisodeServer
declare type EpisodeDetails = {
id: string
// 1, 2, 3, etc.
number: number
url: string
title?: string
}
// Server that hosts the video.
declare type EpisodeServer = {
server: string
headers: { [key: string]: string }
videoSources: VideoSource[]
}
declare type VideoSourceType = "mp4" | "m3u8"
declare type VideoSource = {
url: string
type: VideoSourceType
quality: string
subtitles: VideoSubtitle[]
}
declare type VideoSubtitle = {
id: string
url: string
language: string
isDefault: boolean
}
declare type Settings = {
episodeServers: string[]
supportsDub: boolean
}
Create a typescript (or javascript) file with the following template.
<aside> 🚨 Do not change the name of the class. It must be Provider.
</aside>
/// <reference path="./online-streaming-provider.d.ts" />
class Provider {
getSettings(): Settings {
return {
episodeServers: ["server1", "server2"],
supportsDub: true,
}
}
async search(query: string, dub: boolean): Promise<SearchResult[]> {
return [{
id: "1",
title: "Anime Title",
url: "<https://example.com/anime/1>",
subOrDub: "both",
}]
}
async findEpisodes(id: string): Promise<EpisodeDetails[]> {
return [{
id: "1",
number: 1,
url: "<https://example.com/episode/1>",
title: "Episode title",
}]
}
async findEpisodeServer(episode: EpisodeDetails, _server: string): Promise<EpisodeServer> {
let server = "server1"
if (_server !== "default") server = _server
return {
server: server,
headers: {},
videoSources: [{
url: "<https://example.com/.../stream.m3u8>",
type: "m3u8",
quality: "1080p",
subtitles: [{
id: "1",
url: "<https://example.com/.../subs.vtt>",
language: "en",
isDefault: true,
}],
}],
}
}
}
/// <reference path="./onlinestream-provider.d.ts" />
/// <reference path="./doc.d.ts" />
/// <reference path="./crypto.d.ts" />
class Provider {
api = "<https://anitaku.to>"
ajaxURL = "<https://ajax.gogocdn.net>"
getSettings(): Settings {
return {
episodeServers: ["gogocdn", "vidstreaming", "streamsb"],
supportsDub: true,
}
}
async search(opts: SearchOptions): Promise<SearchResult[]> {
const request = await fetch(`${this.api}/search.html?keyword=${encodeURIComponent(opts.query)}`)
if (!request.ok) {
return []
}
const data = await request.text()
const results: SearchResult[] = []
const $ = LoadDoc(data)
$("ul.items > li").each((_, el) => {
const title = el.find("p.name a").text().trim()
const id = el.find("div.img a").attr("href")
if (!id) {
return
}
results.push({
id: id,
title: title,
url: id,
subOrDub: "sub",
})
})
return results
}
async findEpisodes(id: string): Promise<EpisodeDetails[]> {
const episodes: EpisodeDetails[] = []
const data = await (await fetch(`${this.api}${id}`)).text()
const $ = LoadDoc(data)
const epStart = $("#episode_page > li").first().find("a").attr("ep_start")
const epEnd = $("#episode_page > li").last().find("a").attr("ep_end")
const movieId = $("#movie_id").attr("value")
const alias = $("#alias_anime").attr("value")
const req = await (await fetch(`${this.ajaxURL}/ajax/load-list-episode?ep_start=${epStart}&ep_end=${epEnd}&id=${movieId}&default_ep=${0}&alias=${alias}`)).text()
const $$ = LoadDoc(req)
$$("#episode_related > li").each((i, el) => {
episodes?.push({
id: el.find("a").attr("href")?.trim() ?? "",
url: el.find("a").attr("href")?.trim() ?? "",
number: parseFloat(el.find(`div.name`).text().replace("EP ", "")),
title: el.find(`div.name`).text(),
})
})
return episodes.reverse()
}
async findEpisodeServer(episode: EpisodeDetails, _server: string): Promise<EpisodeServer> {
let server = "gogocdn"
if (_server !== "default") {
server = _server
}
const episodeServer: EpisodeServer = {
server: server,
headers: {},
videoSources: [],
}
if (episode.id.startsWith("http")) {
const serverURL = episode.id
try {
const es = await new Extractor(serverURL, episodeServer).extract(server)
if (es) {
return es
}
}
catch (e) {
console.error(e)
return episodeServer
}
return episodeServer
}
const data = await (await fetch(`${this.api}${episode.id}`)).text()
const $ = LoadDoc(data)
let serverURL: string
switch (server) {
case "gogocdn":
serverURL = `${$("#load_anime > div > div > iframe").attr("src")}`
break
case "vidstreaming":
serverURL = `${$("div.anime_video_body > div.anime_muti_link > ul > li.vidcdn > a").attr("data-video")}`
break
case "streamsb":
serverURL = $("div.anime_video_body > div.anime_muti_link > ul > li.streamsb > a").attr("data-video")!
break
default:
serverURL = `${$("#load_anime > div > div > iframe").attr("src")}`
break
}
episode.id = serverURL
return await this.findEpisodeServer(episode, server)
}
}
class Extractor {
private url: string
private result: EpisodeServer
constructor(url: string, result: EpisodeServer) {
this.url = url
this.result = result
}
async extract(server: string): Promise<EpisodeServer | undefined> {
try {
switch (server) {
case "gogocdn":
console.log("GogoCDN extraction")
return await this.extractGogoCDN(this.url, this.result)
case "vidstreaming":
return await this.extractGogoCDN(this.url, this.result)
default:
return undefined
}
}
catch (e) {
console.error(e)
return undefined
}
}
public async extractGogoCDN(url: string, result: EpisodeServer): Promise<EpisodeServer> {
const keys = {
key: CryptoJS.enc.Utf8.parse("37911490979715163134003223491201"),
secondKey: CryptoJS.enc.Utf8.parse("54674138327930866480207815084989"),
iv: CryptoJS.enc.Utf8.parse("3134003223491201"),
}
function generateEncryptedAjaxParams(id: string) {
const encryptedKey = CryptoJS.AES.encrypt(id, keys.key, {
iv: keys.iv,
})
const scriptValue = $("script[data-name='episode']").data("value")!
const decryptedToken = CryptoJS.AES.decrypt(scriptValue, keys.key, {
iv: keys.iv,
}).toString(CryptoJS.enc.Utf8)
return `id=${encryptedKey.toString(CryptoJS.enc.Base64)}&alias=${id}&${decryptedToken}`
}
function decryptAjaxData(encryptedData: string) {
const decryptedData = CryptoJS.AES.decrypt(encryptedData, keys.secondKey, {
iv: keys.iv,
}).toString(CryptoJS.enc.Utf8)
return JSON.parse(decryptedData)
}
const req = await fetch(url)
const $ = LoadDoc(await req.text())
const encryptedParams = generateEncryptedAjaxParams(new URL(url).searchParams.get("id") ?? "")
const xmlHttpUrl = `${new URL(url).protocol}//${new URL(url).hostname}/encrypt-ajax.php?${encryptedParams}`
const encryptedData = await fetch(xmlHttpUrl, {
headers: {
"X-Requested-With": "XMLHttpRequest",
},
})
const decryptedData = await decryptAjaxData(((await encryptedData.json()) as { data: any })?.data)
if (!decryptedData.source) throw new Error("No source found. Try a different server.")
if (decryptedData.source[0].file.includes(".m3u8")) {
const resResult = await fetch(decryptedData.source[0].file.toString())
const resolutions = (await resResult.text()).match(/(RESOLUTION=)(.*)(\\s*?)(\\s*.*)/g)
resolutions?.forEach((res: string) => {
const index = decryptedData.source[0].file.lastIndexOf("/")
const quality = res.split("\\n")[0].split("x")[1].split(",")[0]
const url = decryptedData.source[0].file.slice(0, index)
result.videoSources.push({
url: url + "/" + res.split("\\n")[1],
quality: quality + "p",
subtitles: [],
type: "m3u8",
})
})
decryptedData.source.forEach((source: any) => {
result.videoSources.push({
url: source.file,
quality: "default",
subtitles: [],
type: "m3u8",
})
})
} else {
decryptedData.source.forEach((source: any) => {
result.videoSources.push({
url: source.file,
quality: source.label.split(" ")[0] + "p",
subtitles: [],
type: "m3u8",
})
})
decryptedData.source_bk.forEach((source: any) => {
result.videoSources.push({
url: source.file,
quality: "backup",
subtitles: [],
type: "m3u8",
})
})
}
return result
}
}