/// /// type EpisodeData = { id: number; episode: number; title: string; snapshot: string; filler: number; session: string; created_at?: string } type AnimeData = { id: number; title: string; type: string; year: number; poster: string; session: string } class Provider { api = "https://animepahe.ru" headers = { Referer: "https://kwik.si" } getSettings(): Settings { return { episodeServers: ["kwik"], supportsDub: false, } } async search(opts: SearchOptions): Promise { const req = await fetch(`${this.api}/api?m=search&q=${encodeURIComponent(opts.query)}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }) if (!req.ok) { return [] } const data = (await req.json()) as { data: AnimeData[] } const results: SearchResult[] = [] if (!data?.data) { return [] } data.data.map((item: AnimeData) => { results.push({ subOrDub: "sub", id: item.session, title: item.title, url: "", }) }) return results } async findEpisodes(id: string): Promise { let episodes: EpisodeDetails[] = [] const req = await fetch( `${this.api}${id.includes("-") ? `/anime/${id}` : `/a/${id}`}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }, ) const html = await req.text() function pushData(data: EpisodeData[]) { for (const item of data) { episodes.push({ id: item.session + "$" + id, number: item.episode, title: item.title && item.title.length > 0 ? item.title : "Episode " + item.episode, url: req.url, }) } } const $ = LoadDoc(html) const tempId = $("head > meta[property='og:url']").attr("content")!.split("/").pop()! const { last_page, data } = (await ( await fetch(`${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=1`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }) ).json()) as { last_page: number; data: EpisodeData[] } pushData(data) const pageNumbers = Array.from({ length: last_page - 1 }, (_, i) => i + 2) const promises = pageNumbers.map((pageNumber) => fetch(`${this.api}/api?m=release&id=${tempId}&sort=episode_asc&page=${pageNumber}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }).then((res) => res.json()), ) const results = (await Promise.all(promises)) as { data: EpisodeData[] }[] results.forEach((showData) => { for (const data of showData.data) { if (data) { pushData([data]) } } }); (data as any[]).sort((a, b) => a.number - b.number) if (episodes.length === 0) { throw new Error("No episodes found.") } const lowest = episodes[0].number if (lowest > 1) { for (let i = 0; i < episodes.length; i++) { episodes[i].number = episodes[i].number - lowest + 1 } } // Remove decimal episode numbers episodes = episodes.filter((episode) => Number.isInteger(episode.number)) // for (let i = 0; i < episodes.length; i++) { // // If an episode number is a decimal, round it up to the nearest whole number // if (Number.isInteger(episodes[i].number)) { // continue // } // const original = episodes[i].number // episodes[i].number = Math.floor(episodes[i].number) // episodes[i].title = `Episode ${episodes[i].number} [{${original}}]` // } return episodes } async findEpisodeServer(episode: EpisodeDetails, _server: string): Promise { const episodeId = episode.id.split("$")[0] const animeId = episode.id.split("$")[1] console.log(`${this.api}/play/${animeId}/${episodeId}`) const req = await fetch( `${this.api}/play/${animeId}/${episodeId}`, { headers: { Cookie: "__ddg1_=;__ddg2_=;", }, }, ) const html = await req.text() const regex = /https:\/\/kwik\.si\/e\/\w+/g const matches = html.match(regex) if (matches === null) { throw new Error("Failed to fetch episode server.") } const $ = LoadDoc(html) const result: EpisodeServer = { videoSources: [], headers: this.headers ?? {}, server: "kwik", } $("button[data-src]").each(async (_, el) => { let videoSource: VideoSource = { url: "", type: "m3u8", quality: "", subtitles: [], } videoSource.url = el.data("src")! if (!videoSource.url) { return } const fansub = el.data("fansub")! const quality = el.data("resolution")! videoSource.quality = `${quality}p - ${fansub}` if (el.data("audio") === "eng") { videoSource.quality += " (Eng)" } if (videoSource.url === matches[0]) { videoSource.quality += " (default)" } result.videoSources.push(videoSource) }) const queries = result.videoSources.map(async (videoSource) => { try { const src_req = await fetch(videoSource.url, { headers: { Referer: this.headers.Referer, "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.56", }, }) const src_html = await src_req.text() const scripts = src_html.match(/eval\(f.+?\}\)\)/g) if (!scripts) { return } for (const _script of scripts) { const scriptMatch = _script.match(/eval(.+)/) if (!scriptMatch || !scriptMatch[1]) { continue } try { const decoded = eval(scriptMatch[1]) const link = decoded.match(/source='(.+?)'/) if (!link || !link[1]) { continue } videoSource.url = link[1] } catch (e) { console.error("Failed to extract kwik link", e) } } } catch (e) { console.error("Failed to fetch kwik link", e) } }) await Promise.all(queries) return result } }