diff --git a/src/extractor/ExtractorRegistry.ts b/src/extractor/ExtractorRegistry.ts index c931303..4458e20 100644 --- a/src/extractor/ExtractorRegistry.ts +++ b/src/extractor/ExtractorRegistry.ts @@ -6,6 +6,7 @@ import { Fetcher } from '../utils'; import { DoodStream } from './DoodStream'; import { Dropload } from './Dropload'; import { SuperVideo } from './SuperVideo'; +import { Soaper } from './Soaper'; import { ExternalUrl } from './ExternalUrl'; import { NotFoundError } from '../error'; @@ -20,6 +21,7 @@ export class ExtractorRegistry { new DoodStream(fetcher), new Dropload(fetcher), new SuperVideo(fetcher), + new Soaper(fetcher), new ExternalUrl(fetcher), // fallback extractor which must come last ]; this.urlResultCache = new TTLCache({ max: 1024 }); diff --git a/src/extractor/Soaper.ts b/src/extractor/Soaper.ts new file mode 100644 index 0000000..114cfec --- /dev/null +++ b/src/extractor/Soaper.ts @@ -0,0 +1,50 @@ +import { Extractor } from './types'; +import { Fetcher } from '../utils'; +import { Context, Meta } from '../types'; + +export class Soaper implements Extractor { + readonly id = 'soaper'; + + readonly label = 'Soaper'; + + readonly ttl = 900000; // 15m + + private readonly fetcher: Fetcher; + + constructor(fetcher: Fetcher) { + this.fetcher = fetcher; + } + + readonly supports = (_ctx: Context, url: URL): boolean => null !== url.host.match(/soaper/) && null !== url.pathname.match(/^\/(episode|movie)_/); + + readonly extract = async (ctx: Context, url: URL, meta: Meta) => { + const movieOrEpisodeId = (url.pathname.match(/\/\w+_(\w+)/) as string[])[1] as string; + + const form = new URLSearchParams(); + form.append('pass', movieOrEpisodeId); + form.append('param', ''); + form.append('extra', ''); + form.append('e2', ''); + form.append('server', '0'); + + const response = await this.fetcher.textPost( + ctx, + new URL(url.pathname.includes('episode') ? '/home/index/GetEInfoAjax' : '/home/index/GetMInfoAjax', url.origin), + form.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }, + ); + const jsonResponse = JSON.parse(response); + + return { + url: new URL(jsonResponse['val'], url.origin), + label: this.label, + sourceId: `${this.id}_${meta.countryCode.toLowerCase()}`, + ttl: this.ttl, + meta, + }; + }; +} diff --git a/src/handler/Eurostreaming.ts b/src/handler/Eurostreaming.ts index 06e0c5d..d4451df 100644 --- a/src/handler/Eurostreaming.ts +++ b/src/handler/Eurostreaming.ts @@ -57,14 +57,14 @@ export class Eurostreaming implements Handler { const html = await this.fetcher.textPost( ctx, new URL('https://eurostreaming.my/index.php'), - { + JSON.stringify({ do: 'search', subaction: 'search', search_start: 0, full_search: 0, result_from: 1, story: imdbId.id, - }, + }), { headers: { 'Content-Type': 'application/x-www-form-urlencoded', diff --git a/src/handler/Soaper.test.ts b/src/handler/Soaper.test.ts new file mode 100644 index 0000000..3d56ebd --- /dev/null +++ b/src/handler/Soaper.test.ts @@ -0,0 +1,55 @@ +import winston from 'winston'; +import { Soaper } from './Soaper'; +import { Fetcher } from '../utils'; +import { ExtractorRegistry } from '../extractor'; +import { Context } from '../types'; +jest.mock('../utils/Fetcher'); + +const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] }); +// @ts-expect-error No constructor args needed +const fetcher = new Fetcher(); +const handler = new Soaper(fetcher, new ExtractorRegistry(logger, fetcher)); +const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { en: 'on' } }; + +describe('Soaper', () => { + test('does not handle non imdb and tmdb series', async () => { + const streams = await handler.handle(ctx, 'series', 'kitsu:123'); + expect(streams).toHaveLength(0); + }); + + test('handles non-existent movies gracefully', async () => { + const streams = await handler.handle(ctx, 'movie', 'tt12345678'); + expect(streams).toHaveLength(0); + }); + + test('handle non-uploaded movie gracefully', async () => { + const streams = (await handler.handle(ctx, 'series', 'tt29141112')).filter(stream => stream !== undefined); + expect(streams).toHaveLength(0); + }); + + test('handle non-existent episode gracefully', async () => { + const streams = (await handler.handle(ctx, 'series', 'tt2085059:99:99')).filter(stream => stream !== undefined); + expect(streams).toHaveLength(0); + }); + + test('handle imdb black mirror s4e2', async () => { + const streams = (await handler.handle(ctx, 'series', 'tt2085059:4:2')).filter(stream => stream !== undefined); + expect(streams).toMatchSnapshot(); + }); + + test('handle tmdb black mirror s4e2', async () => { + const streams = (await handler.handle(ctx, 'series', '42009:4:2')).filter(stream => stream !== undefined); + + const streamsWithoutTtl = streams.map((stream) => { + delete stream.ttl; + return stream; + }); // to avoid flakyness because this is served from cache with lower ttl :) + + expect(streamsWithoutTtl).toMatchSnapshot(); + }); + + test('handle imdb full metal jacket', async () => { + const streams = (await handler.handle(ctx, 'series', 'tt0093058')).filter(stream => stream !== undefined); + expect(streams).toMatchSnapshot(); + }); +}); diff --git a/src/handler/Soaper.ts b/src/handler/Soaper.ts new file mode 100644 index 0000000..d00bb54 --- /dev/null +++ b/src/handler/Soaper.ts @@ -0,0 +1,91 @@ +import * as cheerio from 'cheerio'; +import { Handler } from './types'; +import { + Fetcher, + getTmdbIdFromImdbId, + getTmdbMovieDetails, + getTmdbTvDetails, + parseImdbId, + parseTmdbId, + TmdbId, +} from '../utils'; +import { ExtractorRegistry } from '../extractor'; +import { Context, CountryCode } from '../types'; + +export class Soaper implements Handler { + readonly id = 'soaper'; + + readonly label = 'Soaper'; + + readonly contentTypes = ['movie', 'series']; + + readonly countryCodes: CountryCode[] = ['en']; + + private readonly baseUrl = 'https://soaper.live'; + + private readonly fetcher: Fetcher; + private readonly extractorRegistry: ExtractorRegistry; + + constructor(fetcher: Fetcher, extractorRegistry: ExtractorRegistry) { + this.fetcher = fetcher; + this.extractorRegistry = extractorRegistry; + } + + readonly handle = async (ctx: Context, _type: string, id: string) => { + let tmdbId: TmdbId; + if (id.startsWith('tt')) { + tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, parseImdbId(id)); + } else if (/^\d+:/.test(id)) { + tmdbId = parseTmdbId(id); + } else { + return []; + } + + const [keyword, year, hrefPrefix] = await this.getKeywordYearAndHrefPrefix(ctx, tmdbId); + + const pageUrl = await this.fetchPageUrl(ctx, keyword, year, hrefPrefix); + if (!pageUrl) { + return []; + } + + if (tmdbId.series) { + const html = await this.fetcher.text(ctx, pageUrl); + + const $ = cheerio.load(html); + + const episodePageHref = $(`.alert:has(h4:contains("Season${tmdbId.series}")) a:contains("${tmdbId.episode}.")`).attr('href'); + if (!episodePageHref) { + return []; + } + + const episodeUrl = new URL(episodePageHref, this.baseUrl); + + return [await this.extractorRegistry.handle({ ...ctx, referer: episodeUrl }, episodeUrl, { countryCode: 'en', title: `${keyword} ${tmdbId.series}x${tmdbId.episode}` })]; + } + + return [await this.extractorRegistry.handle({ ...ctx, referer: pageUrl }, pageUrl, { countryCode: 'en', title: keyword })]; + }; + + private readonly fetchPageUrl = async (ctx: Context, keyword: string, year: number, hrefPrefix: string): Promise => { + const searchUrl = new URL(`/search.html?keyword=${encodeURIComponent(keyword)}`, this.baseUrl); + const html = await this.fetcher.text(ctx, searchUrl); + + const $ = cheerio.load(html); + + return $(`.thumbnail .img-group:has(.img-tip:contains("${year}")) a[href^="${hrefPrefix}"]`) + .map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl)) + .get(0); + }; + + private readonly getKeywordYearAndHrefPrefix = async (ctx: Context, tmdbId: TmdbId): Promise<[string, number, string]> => { + if (tmdbId.series) { + const tmdbDetails = await getTmdbTvDetails(ctx, this.fetcher, tmdbId); + + return [tmdbDetails.name, (new Date(tmdbDetails.first_air_date)).getFullYear(), '/tv_']; + } + + const tmdbDetails = await getTmdbMovieDetails(ctx, this.fetcher, tmdbId); + + return [tmdbDetails.title, (new Date(tmdbDetails.release_date)).getFullYear(), '/movie_']; + }; +} diff --git a/src/handler/__snapshots__/Soaper.test.ts.snap b/src/handler/__snapshots__/Soaper.test.ts.snap new file mode 100644 index 0000000..60c97e4 --- /dev/null +++ b/src/handler/__snapshots__/Soaper.test.ts.snap @@ -0,0 +1,45 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Soaper handle imdb black mirror s4e2 1`] = ` +[ + { + "label": "Soaper", + "meta": { + "countryCode": "en", + "title": "Black Mirror 4x2", + }, + "sourceId": "soaper_en", + "ttl": 900000, + "url": "https://soaper.live/home/index/TVM3U8?key=zoeyOo54b7s8NAPnOW0div4OdvmbYxIzpE2RnmAoSXqMAm7eZKs79V4lxEnZsvQ6qYNrOEiqKO4dAwdBiYx3KmwyVRc9oPzLWoOYcLQVArr2j0.m3u8", + }, +] +`; + +exports[`Soaper handle imdb full metal jacket 1`] = ` +[ + { + "label": "Soaper", + "meta": { + "countryCode": "en", + "title": "Full Metal Jacket", + }, + "sourceId": "soaper_en", + "ttl": 900000, + "url": "https://soaper.live/home/index/M3U8?key=VnlzaOr0NYimoY5BLezvsJ8zVLzvdAUVW6XKL711TqadxBBy7AheAYoXRl28s78dmP5WOQUVdxX9RWdOHNRanKJaQ7F5mwRpaWN5fPdMm8M.m3u8", + }, +] +`; + +exports[`Soaper handle tmdb black mirror s4e2 1`] = ` +[ + { + "label": "Soaper", + "meta": { + "countryCode": "en", + "title": "Black Mirror 4x2", + }, + "sourceId": "soaper_en", + "url": "https://soaper.live/home/index/TVM3U8?key=zoeyOo54b7s8NAPnOW0div4OdvmbYxIzpE2RnmAoSXqMAm7eZKs79V4lxEnZsvQ6qYNrOEiqKO4dAwdBiYx3KmwyVRc9oPzLWoOYcLQVArr2j0.m3u8", + }, +] +`; diff --git a/src/handler/index.ts b/src/handler/index.ts index a7c4d92..f7993b5 100644 --- a/src/handler/index.ts +++ b/src/handler/index.ts @@ -5,5 +5,6 @@ export * from './FrenchCloud'; export * from './KinoKiste'; export * from './MeineCloud'; export * from './MostraGuarda'; +export * from './Soaper'; export * from './VerHdLink'; export * from './types'; diff --git a/src/index.ts b/src/index.ts index c0ebcd1..7cf57ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { KinoKiste, MeineCloud, MostraGuarda, + Soaper, VerHdLink, } from './handler'; import { ExtractorRegistry } from './extractor'; @@ -31,6 +32,8 @@ const fetcher = new Fetcher(logger); const extractorRegistry = new ExtractorRegistry(logger, fetcher); const handlers: Handler[] = [ + // EN + new Soaper(fetcher, extractorRegistry), // ES / MX new CineHDPlus(fetcher, extractorRegistry), new VerHdLink(fetcher, extractorRegistry), diff --git a/src/utils/Fetcher.test.ts b/src/utils/Fetcher.test.ts index 49d9121..f90b93b 100644 --- a/src/utils/Fetcher.test.ts +++ b/src/utils/Fetcher.test.ts @@ -41,7 +41,7 @@ describe('fetch', () => { test('textPost ', async () => { fetchMock.post('https://some-post-url.test/', 'some text'); - expect(await fetcher.textPost(ctx, new URL('https://some-post-url.test/'), { foo: 'bar' })).toBe('some text'); + expect(await fetcher.textPost(ctx, new URL('https://some-post-url.test/'), JSON.stringify({ foo: 'bar' }))).toBe('some text'); }); test('head', async () => { diff --git a/src/utils/Fetcher.ts b/src/utils/Fetcher.ts index aa8212c..1f3866d 100644 --- a/src/utils/Fetcher.ts +++ b/src/utils/Fetcher.ts @@ -66,8 +66,8 @@ export class Fetcher { return (await this.cachedFetch(ctx, url, init)).body; }; - readonly textPost = async (ctx: Context, url: URL, data: unknown, init?: CustomRequestInit): Promise => { - return (await this.cachedFetch(ctx, url, { ...init, method: 'POST', body: JSON.stringify(data) })).body; + readonly textPost = async (ctx: Context, url: URL, body: string, init?: CustomRequestInit): Promise => { + return (await this.cachedFetch(ctx, url, { ...init, method: 'POST', body })).body; }; readonly head = async (ctx: Context, url: URL, init?: CustomRequestInit): Promise => { diff --git a/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3findtt0093058external_sourceimdb_id b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3findtt0093058external_sourceimdb_id new file mode 100644 index 0000000..caa3d42 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3findtt0093058external_sourceimdb_id @@ -0,0 +1 @@ +{"movie_results":[{"backdrop_path":"/3k2TRmqMjgt7tcwkYwZQdctnty3.jpg","id":600,"title":"Full Metal Jacket","original_title":"Full Metal Jacket","overview":"A pragmatic U.S. Marine observes the dehumanizing effects the U.S.-Vietnam War has on his fellow recruits from their brutal boot camp training to the bloody street fighting in Hue.","poster_path":"/kMKyx1k8hWWscYFnPbnxxN4Eqo4.jpg","media_type":"movie","adult":false,"original_language":"en","genre_ids":[18,10752],"popularity":9.4881,"release_date":"1987-06-26","video":false,"vote_average":8.123,"vote_count":10828}],"person_results":[],"tv_results":[],"tv_episode_results":[],"tv_season_results":[]} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie1427398 b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie1427398 new file mode 100644 index 0000000..ac5fd00 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie1427398 @@ -0,0 +1 @@ +{"adult":false,"backdrop_path":null,"belongs_to_collection":null,"budget":16,"genres":[],"homepage":"","id":1427398,"imdb_id":"tt12345678","origin_country":["ID"],"original_language":"id","original_title":"Hold & Kick","overview":"","popularity":0.0747,"poster_path":null,"production_companies":[],"production_countries":[],"release_date":"","revenue":12,"runtime":0,"spoken_languages":[],"status":"Released","tagline":"","title":"Hold & Kick","video":false,"vote_average":0.0,"vote_count":0} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie600 b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie600 new file mode 100644 index 0000000..1a32b7d --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie600 @@ -0,0 +1 @@ +{"adult":false,"backdrop_path":"/3k2TRmqMjgt7tcwkYwZQdctnty3.jpg","belongs_to_collection":null,"budget":30000000,"genres":[{"id":18,"name":"Drama"},{"id":10752,"name":"War"}],"homepage":"https://www.warnerbros.com/movies/full-metal-jacket","id":600,"imdb_id":"tt0093058","origin_country":["US"],"original_language":"en","original_title":"Full Metal Jacket","overview":"A pragmatic U.S. Marine observes the dehumanizing effects the U.S.-Vietnam War has on his fellow recruits from their brutal boot camp training to the bloody street fighting in Hue.","popularity":9.4881,"poster_path":"/kMKyx1k8hWWscYFnPbnxxN4Eqo4.jpg","production_companies":[{"id":50819,"logo_path":null,"name":"Natant","origin_country":"US"},{"id":385,"logo_path":null,"name":"Stanley Kubrick Productions","origin_country":"GB"},{"id":174,"logo_path":"/zhD3hhtKB5qyv7ZeL4uLpNxgMVU.png","name":"Warner Bros. Pictures","origin_country":"US"}],"production_countries":[{"iso_3166_1":"GB","name":"United Kingdom"},{"iso_3166_1":"US","name":"United States of America"}],"release_date":"1987-06-26","revenue":46357676,"runtime":117,"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"},{"english_name":"Vietnamese","iso_639_1":"vi","name":"Tiếng Việt"}],"status":"Released","tagline":"In Vietnam, the wind doesn't blow. It sucks.","title":"Full Metal Jacket","video":false,"vote_average":8.123,"vote_count":10828} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie931944 b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie931944 new file mode 100644 index 0000000..093a424 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3movie931944 @@ -0,0 +1 @@ +{"adult":false,"backdrop_path":"/j4RUnJz8QCuWkmB8CPykD5UvgBb.jpg","belongs_to_collection":null,"budget":0,"genres":[{"id":18,"name":"Drama"},{"id":9648,"name":"Mystery"},{"id":36,"name":"History"},{"id":27,"name":"Horror"}],"homepage":"","id":931944,"imdb_id":"tt29141112","origin_country":["AT","DE"],"original_language":"de","original_title":"Des Teufels Bad","overview":"In 1750 Austria, a deeply religious woman named Agnes has just married her beloved, but her mind and heart soon grow heavy as her life becomes a long list of chores and expectations. Day after day, she is increasingly trapped in a murky and lonely path leading to evil thoughts, until the possibility of committing a shocking act of violence seems like the only way out of her inner prison.","popularity":3.7965,"poster_path":"/ycoXsJomPmPjtCfNweM0UWiTkPY.jpg","production_companies":[{"id":25523,"logo_path":"/hVnHNJUHSxaTpQdWPzC8B5CRvns.png","name":"Ulrich Seidl Filmproduktion","origin_country":"AT"},{"id":23406,"logo_path":"/BTYR4mdM8dQxK0cK0gPVhkXd8j.png","name":"Heimatfilm","origin_country":"DE"},{"id":122,"logo_path":"/pbRxJ8oia1MypvLbukeamObtYr2.png","name":"Coop99 Filmproduktion","origin_country":"AT"}],"production_countries":[{"iso_3166_1":"AT","name":"Austria"},{"iso_3166_1":"DE","name":"Germany"}],"release_date":"2024-03-08","revenue":0,"runtime":121,"spoken_languages":[{"english_name":"German","iso_639_1":"de","name":"Deutsch"}],"status":"Released","tagline":"","title":"The Devil's Bath","video":false,"vote_average":6.608,"vote_count":116} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3tv42009 b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3tv42009 new file mode 100644 index 0000000..4dcc617 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:api.themoviedb.org3tv42009 @@ -0,0 +1 @@ +{"adult":false,"backdrop_path":"/dg3OindVAGZBjlT3xYKqIAdukPL.jpg","created_by":[{"id":211845,"credit_id":"52595fb5760ee346619587b5","name":"Charlie Brooker","original_name":"Charlie Brooker","gender":2,"profile_path":"/dmdLd5I4cnpCZzoITm26NOwu6Rn.jpg"}],"episode_run_time":[],"first_air_date":"2011-12-04","genres":[{"id":10765,"name":"Sci-Fi & Fantasy"},{"id":18,"name":"Drama"},{"id":9648,"name":"Mystery"}],"homepage":"https://www.netflix.com/title/70264888","id":42009,"in_production":true,"languages":["en"],"last_air_date":"2025-04-10","last_episode_to_air":{"id":6085099,"name":"USS Callister: Into Infinity","overview":"Nanette Cole and the crew of the USS Callister are stranded in an infinite virtual universe, fighting for survival against 30 million players.","vote_average":7.436,"vote_count":39,"air_date":"2025-04-10","episode_number":6,"episode_type":"finale","production_code":"","runtime":91,"season_number":7,"show_id":42009,"still_path":"/6JxoU4B1VMYf2tt8VPpNl8juKwm.jpg"},"name":"Black Mirror","next_episode_to_air":null,"networks":[{"id":26,"logo_path":"/hbifXPpM55B1fL5wPo7t72vzN78.png","name":"Channel 4","origin_country":"GB"},{"id":213,"logo_path":"/wwemzKWzjKYJFfCeiB57q3r4Bcm.png","name":"Netflix","origin_country":""}],"number_of_episodes":32,"number_of_seasons":7,"origin_country":["GB"],"original_language":"en","original_name":"Black Mirror","overview":"Twisted tales run wild in this mind-bending anthology series that reveals humanity's worst traits, greatest innovations and more.","popularity":84.1507,"poster_path":"/seN6rRfN0I6n8iDXjlSMk1QjNcq.jpg","production_companies":[{"id":76757,"logo_path":null,"name":"House of Tomorrow","origin_country":"GB"},{"id":18663,"logo_path":"/khiCshsZBdtUUYOr4VLoCtuqCEq.png","name":"Zeppotron","origin_country":"GB"},{"id":133905,"logo_path":"/tEEIQIYHQgPyOBgvoAlqPXfpEPX.png","name":"Broke and Bones","origin_country":"GB"}],"production_countries":[{"iso_3166_1":"GB","name":"United Kingdom"}],"seasons":[{"air_date":"2014-12-16","episode_count":1,"id":76596,"name":"Specials","overview":"","poster_path":"/zvfw0f35QmfHCPq5qvdHquFjrQC.jpg","season_number":0,"vote_average":0.0},{"air_date":"2011-12-04","episode_count":3,"id":51964,"name":"Season 1","overview":"Season one of this sci-fi anthology series imagines realities in which people are forced to power their own existence, receive memory implants and more.","poster_path":"/ztdy1f1cAjwsO0GpVKzNuqZ0PMv.jpg","season_number":1,"vote_average":7.7},{"air_date":"2013-02-11","episode_count":3,"id":51965,"name":"Season 2","overview":"This anthology series' second season examines the dark stories of a social media addict, a woman who's part of a live \"life\" show, and more.","poster_path":"/y71DeJiAv0dV8H8hiFnuIuyc0Gx.jpg","season_number":2,"vote_average":7.1},{"air_date":"2016-10-21","episode_count":6,"id":63871,"name":"Season 3","overview":"Lovers meet in a surreal paradise, a young man is tormented by strange texts, and a social app wields disturbing power in these high-tech tales.","poster_path":"/3mKYrlZpFbpFu7CaVxoleO68MFG.jpg","season_number":3,"vote_average":7.7},{"air_date":"2017-12-29","episode_count":6,"id":96276,"name":"Season 4","overview":"A fantasy spins out of control, all-seeing devices expose dark secrets, and a woman flees a ruthless hunter in more tales of technology run wild.","poster_path":"/zvLZvFU6RhdIPEH4afbUa4rmYtW.jpg","season_number":4,"vote_average":7.3},{"air_date":"2019-06-05","episode_count":3,"id":124320,"name":"Season 5","overview":"A video game transforms a longtime friendship, a social media company faces a hostage crisis, and a teen bonds with an AI version of her pop star idol.","poster_path":"/5xjf5aLeooPYWqH66vB80jMJT9u.jpg","season_number":5,"vote_average":6.5},{"air_date":"2023-06-15","episode_count":5,"id":331809,"name":"Season 6","overview":"Twisted tales that span eras — and terrors — deliver a myriad of surprises in this game-changing anthology series' most unpredictable season yet.","poster_path":"/kwATNh8JC1IJclnVwk8x7wZb2b4.jpg","season_number":6,"vote_average":6.4},{"air_date":"2025-04-10","episode_count":6,"id":383778,"name":"Season 7","overview":"","poster_path":"/aRN9LmPQHe8c1B2eR7MB3tiWFWk.jpg","season_number":7,"vote_average":7.1}],"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"}],"status":"Returning Series","tagline":"The future is bright.","type":"Scripted","vote_average":8.285,"vote_count":5522} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordBlackpercent20Mirror b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordBlackpercent20Mirror new file mode 100644 index 0000000..83a04cd --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordBlackpercent20Mirror @@ -0,0 +1,800 @@ + + + + + + + Soaper.live + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

+
+
+ +
+
+
+ + + + + +
+
+ +
+
+
Notifications
+
+
+

If the movie you like is not online, please find it below or manually add an update request.

The administrator will complete the processing within 3 hours.

To prevent abuse of requests, please log in before submitting!

Thank you for your support.

+
+
+
+ +
Related Movies
+
+
+
+
+
+ + +
+

Titles includes keyword 'Black Mirror' :

+
+
+ +
+ +
2018
+
+ + +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+ +
+ +
Related TVs
+
+
+
+
+
+ +
+

Titles includes keyword 'Black Mirror' :

+
+
+
+ +
2011-12-04
+
+ +
+
+
+ + + + + +
+
+ +
+
+
+
+ + +
+
+
+ +
+ + + +
+ +
+
+
+
+ + + + + + + +
+ + +
+ +
+ +
+
Copyright@ 2024 Soaper.live All rights reserved.
+
+
+ +
+
+ + + + + + + + + + + + + + diff --git a/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordFullpercent20Metalpercent20Jacket b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordFullpercent20Metalpercent20Jacket new file mode 100644 index 0000000..6d43588 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordFullpercent20Metalpercent20Jacket @@ -0,0 +1,789 @@ + + + + + + + Soaper.live + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

+
+
+ +
+
+
+ + + + + +
+
+ +
+
+
Notifications
+
+
+

If the movie you like is not online, please find it below or manually add an update request.

The administrator will complete the processing within 3 hours.

To prevent abuse of requests, please log in before submitting!

Thank you for your support.

+
+
+
+ +
Related Movies
+
+
+
+
+
+ + +
+

Titles includes keyword 'Full Metal Jacket' :

+
+
+ +
+ +
1987
+
+ + +
+
+
+ + + + + +
+
+ + + +
+
+
+ +
+ +
+ +
Related TVs
+
+
+
+
+
+     No matches found. + + + + + + +
+
+ +
+
+
+
+ + +
+
+
+ +
+ + + +
+ +
+
+
+
+ + + + + + + +
+ + +
+ +
+ +
+
Copyright@ 2024 Soaper.live All rights reserved.
+
+
+ +
+
+ + + + + + + + + + + + + + diff --git a/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordHoldpercent20percent26percent20Kick b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordHoldpercent20percent26percent20Kick new file mode 100644 index 0000000..10f59f5 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordHoldpercent20percent26percent20Kick @@ -0,0 +1,776 @@ + + + + + + + Soaper.live + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

+
+
+ +
+
+
+ + + + + +
+
+ +
+
+
Notifications
+
+
+

If the movie you like is not online, please find it below or manually add an update request.

The administrator will complete the processing within 3 hours.

To prevent abuse of requests, please log in before submitting!

Thank you for your support.

+
+
+
+ +
Related Movies
+
+
+
+
+
+ +     No matches found. + + + + + + +
+
+ + + +
+
+
+ +
+ +
+ +
Related TVs
+
+
+
+
+
+     No matches found. + + + + + + +
+
+ +
+
+
+
+ + +
+
+
+ +
+ + + +
+ +
+
+
+
+ + + + + + + +
+ + +
+ +
+ +
+
Copyright@ 2024 Soaper.live All rights reserved.
+
+
+ +
+
+ + + + + + + + + + + + + + diff --git a/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordThepercent20Devilpercent27spercent20Bath b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordThepercent20Devilpercent27spercent20Bath new file mode 100644 index 0000000..0cecd79 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:soaper.livesearch.htmlkeywordThepercent20Devilpercent27spercent20Bath @@ -0,0 +1,776 @@ + + + + + + + Soaper.live + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

+
+
+ +
+
+
+ + + + + +
+
+ +
+
+
Notifications
+
+
+

If the movie you like is not online, please find it below or manually add an update request.

The administrator will complete the processing within 3 hours.

To prevent abuse of requests, please log in before submitting!

Thank you for your support.

+
+
+
+ +
Related Movies
+
+
+
+
+
+ +     No matches found. + + + + + + +
+
+ + + +
+
+
+ +
+ +
+ +
Related TVs
+
+
+
+
+
+     No matches found. + + + + + + +
+
+ +
+
+
+
+ + +
+
+
+ +
+ + + +
+ +
+
+
+
+ + + + + + + +
+ + +
+ +
+ +
+
Copyright@ 2024 Soaper.live All rights reserved.
+
+
+ +
+
+ + + + + + + + + + + + + + diff --git a/src/utils/__fixtures__/Fetcher/https:soaper.livetv_r5Wk55Rk1m.html b/src/utils/__fixtures__/Fetcher/https:soaper.livetv_r5Wk55Rk1m.html new file mode 100644 index 0000000..b6d54f4 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/https:soaper.livetv_r5Wk55Rk1m.html @@ -0,0 +1,643 @@ + + + + + + + Soaper.live + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+

+
+
+ +
+
+
+ + + + + +
+
+ +
+
Location : Home >> TV Series >> Black Mirror
+
+ + + +
+
+
+
+
+
+ + + +

Creator :

+

+

+
+

Stars :

+

+

+

Genre :

+

+ Drama, Science Fiction, Thriller

+

Release :

2011-12-04

+

Rating :

0.0 from IMDb

+

Story :

+

+ A television anthology series that shows the dark side of life and technology.

+
+
+

Last Updated :

S7E6:USS Callister: Into Infinity

+
+ + + +
+
+
+
+ +
+
+

Similar TV Series

+
+
+
+
+
+
+ +
2022-09-07
+
+ + +
+
+
+
+
+ +
2023-03-10
+
+ + +
+
+
+
+
+ +
2022-06-16
+
+ + +
+
+
+
+
+ +
2005-09-13
+
+ + +
+
+
+
+
+ +
2022-10-02
+
+ + +
+
+
+
+
+ +
2024-01-31
+
+ + +
+
+
+
+
+ +
+ + + +
+
+ +
+ + +
+ +
+
+ + + +
+
+ + +
+ + + + +
+ + +
+ +
+ +
+
Copyright@ 2024 Soaper.live All rights reserved.
+
+
+ +
+
+ + + + + + + + + + + + + + diff --git a/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetEInfoAjax-pass5KDq78eGp1andparamandextraande2andserver0 b/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetEInfoAjax-pass5KDq78eGp1andparamandextraande2andserver0 new file mode 100644 index 0000000..5f1461c --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetEInfoAjax-pass5KDq78eGp1andparamandextraande2andserver0 @@ -0,0 +1 @@ +{"key":true,"val":"\/home\/index\/TVM3U8?key=zoeyOo54b7s8NAPnOW0div4OdvmbYxIzpE2RnmAoSXqMAm7eZKs79V4lxEnZsvQ6qYNrOEiqKO4dAwdBiYx3KmwyVRc9oPzLWoOYcLQVArr2j0.m3u8","vtt":"https:\/\/ttt5.randchange.top\/\/hls\/2011\/black_mirror\/Season4\/2\/vtt\/\/thumb.vtt","val_bak":"\/home\/index\/TVM3U8?key=zoeyOo54b7s8NAPnOW0div4OdvmbYxIzpE2RnmAoSXqMAm7eZKs79V4lxEnZsvQ6qYNrOEiqKO4dAwdBiYx3KmwyVRc9oPzLWoOYcLQVArr2j0.m3u8","pos":0,"type":"m3u8","subs":[{"path":"\/home\/index\/getsrt?key=AW8O5WowP9HQPAKyN7v1TYMxXYw0EdTEYM3jmJweUwNeBKp7ZRTaJlmXLz8WSX0mewBYr2HjVdOQZLQrCR2L6rwX3Ahd2XbJl2azfEXWywweK6SYEd60rve7FQ4jL7bwWYuWP4n8ZnoQHO9wLoVbvYsKRLB6wlMniLvayBrdEli92e0e6bO9cj8MBorNA5ia00wJ6mpBCnlWO0n9QxhNMz8zqBrJt9PyMErNLeSzoqRzP8yRHAZ43mvYzyIXbLAoOap9f5mz1AoAqxUq7Pep6wL1t1KR6o4rZbI5EWdR5Q12hOwyKWd79EiQMl9jLMONSL1xAOEjaMHe2JE1B.srt","name":"en"},{"path":"\/home\/index\/getsrt?key=rQPyEQNWwYUar7X2lpmxFAYlxA7N35TZza0MEPmOcKM9nmlvZEhZ6BljX75NUYWdZ1X4lKIwaPpNLzN9HyPR9dVn8JhPv6B09vROF7p5E669W8F0mjwEqd3NTrAqR5voPyfm4a8On859sd9rYnJ64wTq1LQmJOM4F621vza9Q3Fq2rprY09qIzK5BqR2vMHMjjp4dmxYCRMy6OR3Veu74XLXZaOECeO79LEVp8ce03AeQY5At4qM2EVwbvT9LM27O5JQur3xYpZplRIWrKe7yq3LhlPJw84aW9swlqbKwvdehbzx9KeRrVuayY5VnydOs2M0N1NPlQI7qO7WzPlaU491YrJ.srt","name":"en:hi"},{"path":"\/home\/index\/getsrt?key=WnzvEnjALphWVnYE4wNMT89LP83lERfxOA8Yo0XRT46LvYBxEPT5n4edrAoWiVrd3wmaWxUXlVWemyeOtaq6b8PxBXC6j8RLqj1rhpBJEqqVayiKq7aQ8bvPiN9LKy4YRoCx1qRJYRyEiNBZXEYn2VH4zoR0jEvYibeL0RVPnzSRzpmpZB3RuWPY9jRbOEH6ooqezN49T2EKXZ2prqu6PRaRONqET2rXJQaR0PfnxWjnPwJjU8Ax0Jp3W1UpMROj2EVKuE6r4xYxRwiJzrV49KnQH9eqMJzLQKcoKveWoBqycapyLMm27dSWYQXmd1dwTvL5P7JP0mte4x9xloBPC9eZ69ov5Nfz2Br3vPoKc3w8powKEQt93en9RNOws5oNYJJ8nJCnL397y4NAFxV2bJ5YrviE9n8JJpxxC8VMomd727ujJwnY9RoncKRb5Z5lQjU8K2.srt","name":"Encoded By nItRo And Uploaded By XpoZ.srt"}],"prev_epi_title":"[S4E1] USS Callister","prev_epi_url":"\/episode_4vGPvjBD31.html?ap=1","next_epi_title":"[S4E3] Crocodile","next_epi_url":"\/episode_PnG6nAnG7v.html?ap=1"} \ No newline at end of file diff --git a/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetMInfoAjax-passd8kdeypDY9andparamandextraande2andserver0 b/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetMInfoAjax-passd8kdeypDY9andparamandextraande2andserver0 new file mode 100644 index 0000000..05da9f4 --- /dev/null +++ b/src/utils/__fixtures__/Fetcher/post-https:soaper.livehomeindexGetMInfoAjax-passd8kdeypDY9andparamandextraande2andserver0 @@ -0,0 +1 @@ +{"key":true,"val":"\/home\/index\/M3U8?key=VnlzaOr0NYimoY5BLezvsJ8zVLzvdAUVW6XKL711TqadxBBy7AheAYoXRl28s78dmP5WOQUVdxX9RWdOHNRanKJaQ7F5mwRpaWN5fPdMm8M.m3u8","vtt":"https:\/\/m3hcsdz313.randchange.top\/\/hls\/1987\/full_metal_jacket\/vtt\/thumbnails.vtt","val_bak":"\/home\/index\/M3U8?key=VnlzaOr0NYimoY5BLezvsJ8zVLzvdAUVW6XKL711TqadxBBy7AheAYoXRl28s78dmP5WOQUVdxX9RWdOHNRanKJaQ7F5mwRpaWN5fPdMm8M.m3u8","pos":0,"type":"m3u8","subs":[{"path":"\/home\/index\/getsrt?key=33pW1rxA7LFoJPBX35b2UM1X39X0Jwhyd3p2BL44SlJVbKKWoNtzb8OJrPLwhp0vzM54dOfvRreQ1KRlCbaEJN2EjeF5qlM9e7d5f3eNYnOLQqtJxKYOwnnyHXJY18J56vf87mzMxlQQfn1rJrwbbQfJ18LPQnmjcWEvxPy1JjF6BO05w2vjuKAz2pAvNmFw160XwbbLC4EN6e5Z7QtwZl2NVP35hMvpMAXvoxumvOem6noLCy6E0l89KnhpKzzvwl8zTl9oNpMmZ7HRAEmAaal5TM3A1RzzXYtEmbWZw77aH2N4J9le6eUQw308RE59fbjRbqRm0mUaPJya1w0a.srt","name":"Arabic"},{"path":"\/home\/index\/getsrt?key=Wnzv5Ore4ZiWVnjlep8zF89WEpWlNwixO0pjr1BBT4637PP0qoto9OQ1qXnPHVrPwL6e4QuXloqvJRlrsaqEPKdEQ2u1l0ZwJ9N1fNnKWBryQMtKqyBp955LhNW2JoWMV9tx1YQWEAMMfNBZLZPrr1C4zP5aBNE9FbRm1Y4Ol7IRzplP95ONsWPnzBPebLUwxO3BwnnPC2ez5alwp6heZ8dxp2MWtdYWda9YRyhnxWjn4bPVF81nLv4rQ6fpo22ey3N2TEajqxvAmPUJzanzdd3wfLOQ3aPPJKFoKO7WzjjJHar5voY8WntWrrOxw5eVTvLJ365V0QT2m3Z1eoBPcPb4.srt","name":"Bengali"},{"path":"\/home\/index\/getsrt?key=Wnzv5Ore4ZiWVnjlep8zF89WEpWlNwixO0pjr1BBT4637PP0qoto9OQ1qXnPHVrPwL6e4QuXloqvJRlrsaqEPKdEQ2u1l0ZwJ9N1fNnKWBryQMtKqyBp955LhNW2JoWMV9tx1YQWEAMMfNBZLZPrr1C4zP5aBNE9FbRm1Y4Ol7IRzplP95ONsWPnzBPebLUwxO3BwnnPC2ez5alwp6heZ8dxp2MWtdYWda9YRyhnxWjn4bPVF81nLv4rQ6fpo22ey3N2TEajqxvAmPUJzanzdd3wfLOQ3aPPJKFoKO7WzjjJHar5voY8WntWVM7r1lYRSvLov4qdv0Se4AeQMOz1U9eZ67KdZrsWq4LXzP6KU3Mazb0ZK0smay983QqNH56975pQJ5.srt","name":"Brazillian Portuguese"},{"path":"\/home\/index\/getsrt?key=98N0wr6XQmHzVw7v9Jb8tqnMAxM6PpHR5zdJbAppCNrJWEE0x6cNQdwPpAbOfdyjW6B9lLf1O4qvB9OXCA7m923mjzCWwaM518qWc6j0yKwZ7of4wMezXvvRtP21dj2QKXtWPnjbVv55C31zrz7bb9hXOBKYJ461TEr0pwJvj4sbnVNW6EZ3UejNRZjrv1UYbx3WYRRjt38PBXxL4zT5n2xNJoY8CqK5qAaKpVF0xRd0v43wTpleoZB6XAf4Pjjead1jfrmpwObRdETaweVwnnjdFyVWZQwwR0uR1a0qWOOBIAnrLPd9bvTZQWjW6poAsjnz5BbKW5fMWOaz6.srt","name":"Czech"},{"path":"\/home\/index\/getsrt?key=4Y753rPOzbSLvBOdjmNMUPV2ne25RqHa8elXPyYYSEpleKKMW4U97V25j6AKUomR05NAKQSJ5vdqLe5lsozmxLrmKpsN8ZwjYbVNS6bPJ91my8fYM4XKdJJLIj2PX529d1CwK54AEM22CaJ3R3Nnn2i0oQ7ydxlmUa40BvQLEKUAZwdlP9JKsdbB72bneziRm0bNRAAPUY1WQLMKeBtBbL0w2xrKFAlpA1ole9u2nqE24moAFl9BVxArqjtoXrrAJl1rij4xo5MJ2VFWqV6qwwmAHa73XO22V1i4Q23nRZZBF7Mr3Rqzdqcn10Q8rBEWFN8Qebo9nriWBbLW24qW.srt","name":"Danish"},{"path":"\/home\/index\/getsrt?key=pblyPQEJBAHrqoxjOb0EtEQvVyv8N9SOXzW8y1JJTonRbBBVq3IbXAe028lwupBanElXbPf5rZy6LPrKcoj9ePK9yMs8za2PZV58SdYmpAzEWlCAYxvP1nnRI2jw9zj04AceqPpJORvvh1Yv8vjrr0iqBl4v56K7faLWZX1oYBU3yEP9We2XHMELX4E3Qluyo89dyJJmIR08Kl9PozCaP01Z8OEKc4l54qVlepc8r1a864QeUMpYvQAxOXS49LLZvwoLfJObxA8p4lF35yV5KKr8H73nXabbNxHOd0vzxBBYFZ1wVXBrEBulxNNjPdE9i8PrB3pnNBIbBV1JE.srt","name":"Dutch"},{"path":"\/home\/index\/getsrt?key=QnqwdOrQ06i94AeNyZxOiXEyK3y95ZFmMXan93KKi9QpK443Lrcd51ZnM6xwh71x0KWlezUAoep37RoMhwXALmYAE5SJz0qYMy7Jiml4B61wJLSbPQBX3yyKu7j9NljpyAsZxwBNPeRRt9pLnLbEE1s5MambyxEeIXQ7Ylv0K5u83KEOWV4Zf9A8qpAWayHXyPBKXddMU20VNvpwMlhRj29mAQ6bCoq8oMEqvrUpqeWpxEwPF2J74yBAOQInEQQW8XVQupQVKzMR0aSON90N55LRTw54EmzzJot04Ajbo669HL51PW8nKjS7QLW1QzRXi15p1ONQYotvLZloJq3jtB3o.srt","name":"English"},{"path":"\/home\/index\/getsrt?key=8wVNJrPpnjtWjz16PJZBFw4AQYAq7rUvy85dl3wwC0p2Rjj48MIoOrb8V9AYHWP7ABR92xHnOp3RQAOes41pLZnpyYU9ayr2QXm9uK2lbJV7yYSROZ67qMMKcZ4byo4WMAhKb8wp7d44COj1X1Z77zI2XZ831Oo9c86NQxb7RltRe03pZn9dsy9VnY9OQ1tonO27oEEyCNqL57XbWrhdx2jmX7aJUVNdVyKNR5uPKwyPr5b0HzoW79xOX2CjQxxERBdxHbnKX0dZ3ET3pAepyy1QHKa7mx99yqIXL04JzNNVszAvE7wMJ8fbW9VrAnwnHVlRVZw925tXaaqL9L1WsPmqPYKzVWsKB0a3JXErcBMV.srt","name":"Farsi\/Persian"},{"path":"\/home\/index\/getsrt?key=NKAZ6OxLNrTr3olJYZ1wtompqlp7Mxudj06A8reeh1Lr6oojbEun36pMZRQzhpKV9WaR2wfmy0jKR6yqu64nOWbnd3Fanq3v6Y5aCeZ4WA16mqh6OeWJXjjEI4LdW9LR3KtRe9Kl0r44S2qW0WdZZ6f1PdxXBaA7fEYqwWP5ROsPM4njp9wxH4PeOVPnY1U7xOPe7yyVhEqAJ7mNRysn7o8WaqX3I1381nN32yUl0BKlMq6WTaO8m1wyY4H5JaaBb82aiABN3zdpO9Iex51xLLnEUzo6jX33xviE5W1v2ee8HN8wa9x7vjuANpAK9rXjip2LpOnobXh4jorJRanwCn2w.srt","name":"Finnish"},{"path":"\/home\/index\/getsrt?key=b154zYEpdbt94jybNrn6iYXmprm4azu0lXb95ZMMiBj05zz3MYtXBmdEneJWSO5Zm8ANMoI6wXM753wVhQJwYA1wOdFmjZpEKX5mt2m7N1WRjMuMrvlRKee4f5Lyq4Llz7TwmYryA5EECVwB7BzrrpcX5RyY1dEMTP9xJ1mv3Vtnm0obPjZOSVb1A3b7JNFKVpwMK77mC1KeA9MPWYTNL7PlV9BQTrRXrQ8RWJCJ6obJKwYdsO2p5Vlwm1U7Q00BxMO0SJlQxbmW4RFr0Wy0OOEeul0Q7YNNOmT28rYX7005FodxYBL7lqhPMxpJBN6VU14803PovqU3V0X3Yoa3.srt","name":"French"},{"path":"\/home\/index\/getsrt?key=Lre3POxzK6S4O1wVpNX9uLd3Rw3yMji8z73jWnaau1MxyBBRz3u6dbr0xPABS5PMRB17X2iz68JlBy6LU3wpa6NpYoiYRxKbvMjYt3ZKJ4Lp1WtnjwVqr005H640xE4POMhJNVoYnaKKhWoJKJLVV5s15N2zjBYOf7pyVMlNqPs0LNnK3MRACpZJ4oZnB0U0wNol0LLZFNJb6dweXBh17oa4lRbyhLZbL3BZnVf0QEP05dbBTMxl2N7LPWSn7oodPrKou1QBAMRl8jCyz9pz00mOTBXlMK33Z8Swz8nZ3yy9sMWNn6y1lYtemmdL1LaeCdAvR4q453SwzlLwB2pw.srt","name":"German"},{"path":"\/home\/index\/getsrt?key=Knj1oKx7Npiqn0v21NjVS5jwAdw18LfbV2dqQY44hvbmLooPM9c2YLlOveE5S0MvO74wQJfJAV5NjpAZs3J2awQ2PoibOdqm9Y7bfBAz3XlPYpt1Nj3vELLZcd0Zam0lXquXpQnVby33Cq4YxY9XXVfyrY321PObIjqaoyO6NeCArejlNnQPsdpz4Qp5aPiwBmZawrr1Ca0Zjbe8qzcq3navQ46YuVOYVpwOX7u52Q35alAmflxvP3aJRotwMqqVmKNqsMKEnr1e42UVKwXK33Y0Sz1ORAJJjoirmdLNRaaAc6w2zm5L4ouQ44P73o9LcL885raaolTwnpMw0E3w.srt","name":"Hebrew"},{"path":"\/home\/index\/getsrt?key=vNdyBeAzXqFoebBryX5nUMN0nl0Ja2h3VjardlJJiBY03mm7PMt5Vbl6ZmBaswRNAe81ByHL3PQ0v53efbj1pJ01BMFJmeZNM1bJiarmlOQo6pIOozyXejj4cRe24ze6vjIAo8d4zy99fb2qLqX99nhb9Yy78w4Ltex4pyP3AdCbRpPMAXn7UlwANxwQrmUlvX4wlBBbu957JMVdlEiQnzJjEB4oh0dR078dJVfVqL8VKMlmHJ8nxNMXpdC9aVVQp73VSBbnyZ76QJFEYelYMMR7s7eEJKOOxXHaz7bn3VVLHOpqwYL7dQc4YjOqLop9sQNzdP5WdYT8Xx8NOl5oU4NrRXAWvQCrP7.srt","name":"Indonesian"},{"path":"\/home\/index\/getsrt?key=yo9yRaAb1qTrz16XadnWt1dxX9xrEKh6y14Pvm33FylmAWWRa4Tzwa67qLXvhem0jzXvAWFzBP7NeWBmUyZzwJPznViOxQ8jlnbOhdLMWZP3O5CBy3Rbpvv0H50OX80bQyTMl6dOZmEEILQdmd5vvnf4jOWeprq3FWymrN5lq4FPxYO4nM0KHXjPd6jvyETx7aWMxLLvUNmXd9q43ohd1Nnj4AlQUMWVMeyW0xuWn9YWPZ8otbpeXZ0AyKfZmjjWPE9jtYLdErNla9FPvnbv22zNUnVXjl11pqF8J2rbvoodswbJaArY5WIxneEnooPWHWQvWdbnY8fXMPBZyNqvhwqQ.srt","name":"Italian"},{"path":"\/home\/index\/getsrt?key=oLmroWyMpqSpdnmVMwYJcqNLxwL213H1jdQPV7ZZF4mp2qqzKYtrbR504zQjhXPmAL3a8lCyQm1MO3QXfzyoQ6YonvFwpe8KmX5wcW6ox7az05Fe1drVWoo7tm5W8V56Kyf5YaEm6KXXCAoj4jKOOwT3QnBZ25PRt1O0rYvAd9I7vrEMAeR8sBVQn3VwabUWapmyWYYRuNwnjAlM7phVpdAQERzKs2b827ebyxcr4yzrMK35IXvmpYx45yClQoo2YVaofP9NEe5A1KSmM4KM55xoizQmO84476i98dKL1NNpFRK59yJee7FX3BEm8Wn6SwMaPBKrPOiK773Zz945IxEvX54.srt","name":"Japanese"},{"path":"\/home\/index\/getsrt?key=0R4vprqP87Tordyxn2vlUxzNeYN5WnSlvrXQNn11SzV7w33MymtnQqBX0ErjhQoWqEx2MpSz34Wr8l3VUMKYoJ3Yj2ix0qOeZKAxUBna9oOR81teBX6pbyyqt149xa4YBEIKWQBL5O66CodVZV7wwRSx8jQKLVPzi9J2ymNeLjHlNyQP0Em2CrwJLQwNm7iQyzdVQjj5IMyEoqjpBnT2A7z6Ee85SJl5Jm1lw8cbR54bBqlKS8wB07yAnNf64RR5WzqRc2q9dr510xUOnNen44RlTPRMLzaa0piKERBja88msOlVo8y55qhRdwqvMEEwI5ENWQaQ9XCO537OowLO.srt","name":"Korean"},{"path":"\/home\/index\/getsrt?key=VnlzaOr0NYimoY5BLezvsJ8zVLzvdAUVW6XKL711TqadxBBy7AheAYoXRl28s78dmP5WOQUVdxX9RWdOHNRanKJaQ7F5mwRpaWN5f5rwn7lVNJSordN5166lfl3JQ73ZPRtKpWber6qqC9EdKdJvvPsOjaq9xbz8tqmadJLe9ph2O74Y6EJxs3wQZVw62XHX8QA6XOORUmRME2KJAXIPB7rz1WybF4p54qApVPc2pxM23YLrFJrlWLwbzxCE5xxNdV1xUjo1WX5RY8Fa6zK6jjbwFr3NQV77enUZjrb5p77wt1jxr4MnnNS7NxQ4dPbjUabxMzbeBbFX51orb2ryS5W0L5Jjw5.srt","name":"Malayalam"},{"path":"\/home\/index\/getsrt?key=vNdyBeAzXqFoebBryX5nUMN0nl0Ja2h3VjardlJJiBY03mm7PMt5Vbl6ZmBaswRNAe81ByHL3PQ0v53efbj1pJ01BMFJmeZNM1bJiarmlOQo6pIOozyXejj4cRe24ze6vjIAo8d4zy99fb2qLqX99nhb9Yy78w4Ltex4pyP3AdCbRpPMAXn7UlwANxwQrmUlvX4wlBBbu957JMVdlEiQnzJjEB4oh0dR078dJVfVqL8VKMlmHJ8nxNMXpdC9aVVQp73VSBbnyZ76QJFEYelYMMR7s7eEJKOOxXHaz7bn3VVLHOpqwYL77QU4YA1VR2xxIQNN3L6LK1C8XKj9L91ZCAodaArZBA.srt","name":"Norwegian"},{"path":"\/home\/index\/getsrt?key=BQwdWrx5mOF6235vAM79h90bwBbydxFEyZq4xr00T2z0d115xac3bedXomBQiazKe5vAEXCjl1qp2ElPfROKbolKm0FvN3yljZKvi6QwlObqrZfpXV5Qd66ZUQK8e7K2XbHBq47lN2yyh4pYRYodd1uyYbwpVXoZIKm2PvEp8lFE9n470YK1Fad7Pqd3QmHRnz3jR99WU4RpdrY8QntWRYvN8bX4Im57myP5W4f4ea145oPvIeWx91VovNCvWPPoY4mPHXoW8PK6V3Uy9Br9vvYeT7RJ3Qyya2HZm1Qy4eent4NVxozyZ4fbqz1OPXMzcL1yNM7eVOi4lYv4Bwd4.srt","name":"Polish"},{"path":"\/home\/index\/getsrt?key=zoeya1AW3qT8NAmaQRoMtv41Yj1bWRizpm8V5lLLHXqxNKK08Otn5JKAO89QhvQPY5mx2LcqKEr5nLKzCYxbwv1bdoiO4rx3RjwOhnwzmQE0WBfBKW214LLzHAz9N7zp0Bs0Vb8e6Z99TpyYPYBVV5hmZ27R8vpeIn4ElmwjV5cBqymJYKaxsQ8xqX8oEMibL0PpbZZVcvjaO4bXMyfzyl9Z43rEC36Q3rj6nYHJr1RJyx0XsVd5YN8K7vcK6ZZvNMmZiLRYZdpOQETm3yM3YYwpia3MXxeeqOi2ZxpWqMMrFwMYjv0pJwSPvKE84wlZhy622Pdp7YcX77rBrr8oup524vWoO0f8R9.srt","name":"Portuguese"},{"path":"\/home\/index\/getsrt?key=zoeya1AW3qT8NAmaQRoMtv41Yj1bWRizpm8V5lLLHXqxNKK08Otn5JKAO89QhvQPY5mx2LcqKEr5nLKzCYxbwv1bdoiO4rx3RjwOhnwzmQE0WBfBKW214LLzHAz9N7zp0Bs0Vb8e6Z99TpyYPYBVV5hmZ27R8vpeIn4ElmwjV5cBqymJYKaxsQ8xqX8oEMibL0PpbZZVcvjaO4bXMyfzyl9Z43rEC36Q3rj6nYHJr1RJyx0XsVd5YN8K7vcK6ZZvNMmZiLRYZdpOQETm3yM3YYwpia3MXxeeqOi2ZxpWqMMrFwMYjv0pJzTPvKEV3E7QFya4zPNXeVFX7VKzrRdEtjQMRlb.srt","name":"Romanian"},{"path":"\/home\/index\/getsrt?key=nv0K4bE5Bqs8wZ21B7nYtrneJyeX56Sn4KMEZRrrhxWL3bbzpmIRXKoQqj2ls7qrjZm9Q3UQandN4AaBfM0EJK6E3Wi0Q2xqze60Fx148emzOycabdv12qq9c7PMneP25dsroEM46neehBYr9rKyyPtXW0lYOvVNTKYM9ZJn64FENMnldXyLFqmXAEmaJjujBxqVjyydIlqZrMJEdBuZYaMorV6wTxKwxMBKL3I9w8e9zVa1C1n6A90Mb5ClK11Ow031fbaQorZM8yTB3xA3qqz7CdVX2nWWvmS9wpbjdnnaFp63rlxVqzs0LBaA3jMziK9916RxYluA4pR9d9L8Uq2KJqpoZq.srt","name":"Slovenian"},{"path":"\/home\/index\/getsrt?key=zoeya1AW3qT8NAmaQRoMtv41Yj1bWRizpm8V5lLLHXqxNKK08Otn5JKAO89QhvQPY5mx2LcqKEr5nLKzCYxbwv1bdoiO4rx3RjwOhnwzmQE0WBfBKW214LLzHAz9N7zp0Bs0Vb8e6Z99TpyYPYBVV5hmZ27R8vpeIn4ElmwjV5cBqymJYKaxsQ8xqX8oEMibL0PpbZZVcvjaO4bXMyfzyl9Z43rEC36Q3rj6nYHJr1RJyx0XsVd5YN8K7vcK6ZZvNMmZiLRYZdpOQETm3yM3YYwpia3MXxeeqOi2ZxpWqMMrFwMYjv0pJacP9pxEEvQ4syaNyVL2zbfxMw2JX0j3sn2m.srt","name":"Spanish"},{"path":"\/home\/index\/getsrt?key=8wVNJrPpnjtWjz16PJZBFw4AQYAq7rUvy85dl3wwC0p2Rjj48MIoOrb8V9AYHWP7ABR92xHnOp3RQAOes41pLZnpyYU9ayr2QXm9uK2lbJV7yYSROZ67qMMKcZ4byo4WMAhKb8wp7d44COj1X1Z77zI2XZ831Oo9c86NQxb7RltRe03pZn9dsy9VnY9OQ1tonO27oEEyCNqL57XbWrhdx2jmX7aJUVNdVyKNR5uPKwyPr5b0HzoW79xOX2CjQxxERBdxHbnKX0dZ3ET3pAepyy1QHKa7mx99yqIXL04JzNNVszAvE7wM89Hbq7nYPAYmHVlRVNoAvYCBve1wX79JcvLA.srt","name":"Swedish"},{"path":"\/home\/index\/getsrt?key=Knj1oKx7Npiqn0v21NjVS5jwAdw18LfbV2dqQY44hvbmLooPM9c2YLlOveE5S0MvO74wQJfJAV5NjpAZs3J2awQ2PoibOdqm9Y7bfBAz3XlPYpt1Nj3vELLZcd0Zam0lXquXpQnVby33Cq4YxY9XXVfyrY321PObIjqaoyO6NeCArejlNnQPsdpz4Qp5aPiwBmZawrr1Ca0Zjbe8qzcq3navQ46YuVOYVpwOX7u52Q35alAmflxvP3aJRotwMqqVmKNqsMKEnr1e42UVKwXK33Y0Sz1ORAJJjoirmdLNRaaAc6w2zm5LAPuQ47bvq6ajTdneRoLXKZt90m.srt","name":"Thai"},{"path":"\/home\/index\/getsrt?key=0R4vprqP87Tordyxn2vlUxzNeYN5WnSlvrXQNn11SzV7w33MymtnQqBX0ErjhQoWqEx2MpSz34Wr8l3VUMKYoJ3Yj2ix0qOeZKAxUBna9oOR81teBX6pbyyqt149xa4YBEIKWQBL5O66CodVZV7wwRSx8jQKLVPzi9J2ymNeLjHlNyQP0Em2CrwJLQwNm7iQyzdVQjj5IMyEoqjpBnT2A7z6Ee85SJl5Jm1lw8cbR54bBqlKS8wB07yAnNf64RR5WzqRc2q9dr510xUOnNen44RlTPRMLzaa0piKERBja88msOlVo8y5q0uROKK8X7qms5El5ywrobFj8o96OlAVSBrl.srt","name":"Turkish"},{"path":"\/home\/index\/getsrt?key=xoQypRALaqTPEXZjQAzbhKbm8lmzAZuaW70q5zXXSryjJvv32XI6W8QwvrPLSvK4z7A98wcyQY7jpnQLf7LW5QAW2NuRvq9MzjwRtn02OpMLQjfM9yX56BBYfKAPoaAWbef0MbL1vyPPTReyoyqQQdhoLYr6XaJefvxepWLRNmclQbzWJoqjCraNdvaXn0ixArlNxeezUAaw4v35deh30p2NVaABI8Pw8ErPZ5fxjVaxNYK5tlaBoNzQRntB3ooJKr0oUXZYr9N17eUeEWYEXXjNUldO4AqqE7TevR1p9YYwSOrBoK6Pm9i0QVNlvQ0mTwLB2apNd3tPWL.srt","name":"Urdu"},{"path":"\/home\/index\/getsrt?key=EmYpjnxMQ6SPwER2jqV4haLNOwNxJoI3rJQ7LPVViNKnLBBAM2czQbYL9a27hBOLyEr0n5t1bwRNA4bZC2y7vJP7n6sZ3zYWO7lZH61WZ8R4LBfzmexbLjjotndmzEdxWvIrN9VbaM88hAp6z6L22OT1XJp52BRvfN15WP6B9Rh84zy05pVdfJX4oWXBL5FVb4BnVQQwIMXKmdP3a4TqayEXlWLou63q6Yb3vmuNVE0NYpZ6Uljw9rdo0Vt0orr3JWzrupQ9X37NLJS9J3jJOOrvT7WJnwllYQHB83jElMMmcJ1bz7nLMWfQ9rQORjXBtew9Xr3ZEEf0XXW5WW38sn3NMjPpJbtpvz.srt","name":"Vietnamese"}],"ip":"130.61.236.204"} \ No newline at end of file diff --git a/src/utils/__mocks__/Fetcher.ts b/src/utils/__mocks__/Fetcher.ts index 3fab513..494295c 100644 --- a/src/utils/__mocks__/Fetcher.ts +++ b/src/utils/__mocks__/Fetcher.ts @@ -18,10 +18,10 @@ class MockedFetcher { return this.fetch(path, ctx, url, init); }; - readonly textPost = async (ctx: Context, url: URL, data: unknown, init?: RequestInit): Promise => { - const path = `${__dirname}/../__fixtures__/Fetcher/post-${slugify(url.href)}-${slugify(JSON.stringify(data))}`; + readonly textPost = async (ctx: Context, url: URL, body: string, init?: RequestInit): Promise => { + const path = `${__dirname}/../__fixtures__/Fetcher/post-${slugify(url.href)}-${slugify(body)}`; - return this.fetch(path, ctx, url, { ...init, method: 'POST', body: JSON.stringify(data) }); + return this.fetch(path, ctx, url, { ...init, method: 'POST', body }); }; readonly head = async (ctx: Context, url: URL, init?: RequestInit): Promise => { diff --git a/src/utils/tmdb.ts b/src/utils/tmdb.ts index 8ca444c..cbad2a8 100644 --- a/src/utils/tmdb.ts +++ b/src/utils/tmdb.ts @@ -18,6 +18,16 @@ interface ExternalIdsResponsePartial { imdb_id: string; } +interface MovieDetailsResponsePartial { + release_date: string; + title: string; +} + +interface TvDetailsResponsePartial { + first_air_date: string; + name: string; +} + export const parseTmdbId = (id: string): TmdbId => { const idParts = id.split(':'); @@ -69,3 +79,11 @@ export const getImdbIdFromTmdbId = async (ctx: Context, fetcher: Fetcher, tmdbId tmdbImdbMap.set(tmdbId.id, response.imdb_id); return { id: response.imdb_id, series: tmdbId.series, episode: tmdbId.episode }; }; + +export const getTmdbMovieDetails = async (ctx: Context, fetcher: Fetcher, tmdbId: TmdbId): Promise => { + return await tmdbFetch(ctx, fetcher, `/movie/${tmdbId.id}`) as MovieDetailsResponsePartial; +}; + +export const getTmdbTvDetails = async (ctx: Context, fetcher: Fetcher, tmdbId: TmdbId): Promise => { + return await tmdbFetch(ctx, fetcher, `/tv/${tmdbId.id}`) as TvDetailsResponsePartial; +};