Type

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
}

Code

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,
                }],
            }],
        }
    }
}

Example

/// <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
    }
}