Spaces:
Running
Running
import bytes from "bytes"; | |
import fetch from "cross-fetch"; | |
import { load, CheerioAPI, Element } from 'cheerio'; | |
export type EpisodeType = { | |
showLink: string | undefined; | |
title: string; | |
magnet: string | undefined; | |
torrent: string | undefined; | |
size: number; | |
released: string; | |
seeds: number; | |
} | |
export type ShowType = { | |
title: string; | |
summary: string; | |
description: string; | |
imdbId: string | null; | |
episodes: EpisodeType[]; | |
} | |
export type ImdbEpisodeType = { | |
id: number; | |
hash: string; | |
filename: string; | |
episode_url: string; | |
torrent_url: string; | |
magnet_url: string; | |
title: string; | |
imdb_id: string; | |
season: string; | |
episode: string; | |
small_screenshot: string; | |
large_screenshot: string; | |
seeds: number; | |
peers: number; | |
date_released_unix: number; | |
size_bytes: string; | |
} | |
export type ApiResponseType = { | |
imdb_id?: string; | |
torrents_count: number; | |
limit: number; | |
page: number; | |
torrents: ImdbEpisodeType[]; | |
} | |
/** | |
* Crawl any given url | |
* | |
* @link https://www.npmjs.com/package/crawler | |
* @param url | |
* @returns `Crawler.CrawlerRequestResponse` | |
*/ | |
async function crawl(url: string) { | |
const body = await fetch(url).then(async resp => resp.text()); | |
const $ = load(body); | |
return { body, $ }; | |
} | |
/** | |
* Get all shows listen on EZTV | |
* | |
* @returns `object` | |
*/ | |
export async function getShows() { | |
const { $ } = await crawl(`https://eztv.wf/showlist/`); | |
const shows = $('a[class="thread_link"]').toArray(); | |
return shows.map(show => { | |
const showIdRegex = $(show).attr('href')?.match(/shows\/(\d+)\//); | |
return { | |
id: showIdRegex ? parseInt(showIdRegex[1]) : null, | |
title: $(show).text() | |
} | |
}).filter(show => show.id) as { id: number, title: string }[]; | |
} | |
/** | |
* Get a show and its episodes | |
* | |
* Recommended to use an ID, if using a showname | |
* it must be an exact match except for uppercase | |
* | |
* @param showId - A show ID or a show name | |
* @returns `object` | |
*/ | |
export async function getShow(show: number|string): Promise<ShowType> { | |
/** | |
* If a string is passed find show based on title | |
*/ | |
if(typeof show === 'string') { | |
const shows = await getShows(); | |
const findShow = shows.find(s => s.title.toLowerCase() === show.toLowerCase()); | |
if (!findShow) { | |
throw new EztvCrawlerException(`Did not find a show with name ${show}`) | |
} | |
return getShow(findShow.id); | |
} | |
const { $ } = await crawl(`https://eztv.wf/shows/${show}/`); | |
const episodes = $('[name="hover"]').toArray(); | |
const imdbIdRegex = $('[itemprop="aggregateRating"] a').attr('href')?.match(/tt\d+/); | |
const result = { | |
title: $('.section_post_header [itemprop="name"]').text(), | |
summary: $('[itemprop="description"] p').text(), | |
description: $('span[itemprop="description"] + br + br + hr + br + span').text(), | |
imdbId: imdbIdRegex ? imdbIdRegex[0] : null, | |
episodes: episodes.map(episode => { | |
return transformToEpisode($, episode); | |
}) | |
} | |
if (!result || !result.title || result.title === '') { | |
throw new EztvCrawlerException(`Did not find a show with name ${show}`) | |
} | |
return result; | |
} | |
/** | |
* Search for a TV show episode | |
* | |
* @param query - string | |
* @returns `episodeObject` | |
*/ | |
export async function search(query: string) { | |
const { $ } = await crawl(`https://eztv.wf/search/${query}`); | |
const episodes = $('[name="hover"]').toArray(); | |
return episodes.map(episode => { | |
return transformToEpisode($, episode); | |
}) | |
} | |
/** | |
* Get a list of torrents | |
* | |
* @param limit | |
* @param page | |
* @param apiBaseUrl - If eztv domain changed or eztv is blocked in your country provide a proxy url here | |
* @returns `ApiResponseType` | |
*/ | |
export async function getTorrents(limit = 10, page = 1, apiBaseUrl = 'https://eztv.wf/api/') { | |
return await makeApiRequest('/get-torrents', { limit: limit.toString(), page: page.toString() }, apiBaseUrl); | |
} | |
/** | |
* Get a list of torrents based on IMDb ID | |
* | |
* NOTE: | |
* For TV Shows provide the IMDb id of the show itself, it does not work | |
* when you provide an IMDb for individual episodes | |
* | |
* @param imdbId - IMDb ID | |
* @param apiBaseUrl - If eztv domain changed or eztv is blocked in your country provide a proxy url here | |
* @returns `ApiResponseType` | |
*/ | |
export async function getTorrentsByImdbId(imdbId: string, limit = 30, page = 1, apiBaseUrl = 'https://eztv.wf/api/') { | |
return await makeApiRequest('/get-torrents', { imdb_id: imdbId, limit: limit.toString(), page: page.toString() }, apiBaseUrl); | |
} | |
/** | |
* Send a request to EZTV's API | |
* | |
* @param path | |
* @param params | |
* @param apiBaseUrl | |
* @returns `ApiResponseType` | |
*/ | |
async function makeApiRequest(path: string, params: Record<string, string>, apiBaseUrl = 'https://eztv.wf/api/') { | |
if (params.imdb_id) { | |
params.imdb_id = params.imdb_id.replace(/\D+/, ''); | |
} | |
try { | |
const request = await fetch(`${apiBaseUrl}/${path}?${new URLSearchParams(params)}`); | |
const json: ApiResponseType = await request.json(); | |
return json; | |
} catch(e) { | |
if(e instanceof Error) { | |
throw new EztvCrawlerException(e.message); | |
} | |
throw new EztvCrawlerException('Could not fullfill request.'); | |
} | |
} | |
/** | |
* Transforms a <table> Element into a episode object | |
* | |
* Note: in torrents, don't parse the `.download_2` class, it contains spam and malware in some cases | |
* | |
* @param $ - cheerio.CheerioAPI | |
* @param episode - cheerio.Element | |
* @returns `episodeObject` | |
*/ | |
function transformToEpisode($: CheerioAPI, episode: Element) { | |
return { | |
showLink: $(episode).find('td:nth-child(1) a').attr('href'), | |
title: $(episode).find('td:nth-child(2)').text()?.replace(/\n/g, ''), | |
magnet: $(episode).find('td:nth-child(3) .magnet').attr('href')?.replace(/\n/g, ''), | |
torrent: $(episode).find('td:nth-child(3) .download_1').attr('href')?.replace(/\n/g, ''), | |
size: bytes($(episode).find('td:nth-child(4)').text()), | |
released: $(episode).find('td:nth-child(5)').text(), | |
seeds: parseInt($(episode).find('td:nth-child(6)').text()) || 0 | |
} | |
} | |
export class EztvCrawlerException extends Error {} |