refactor: simplify id usage and imdb <-> tmdb transformations

This commit is contained in:
WebStreamr 2025-06-08 13:50:53 +00:00
parent 8724e04860
commit fbae3924e5
No known key found for this signature in database
24 changed files with 107 additions and 168 deletions

View file

@ -2,7 +2,7 @@ import { Request, Response, Router } from 'express';
import winston from 'winston';
import { Handler } from '../handler';
import { Config, Context } from '../types';
import { envIsProd, StreamResolver } from '../utils';
import { envIsProd, ImdbId, StreamResolver } from '../utils';
import { ContentType } from 'stremio-addon-sdk';
export class StreamController {
@ -37,7 +37,7 @@ export class StreamController {
const handlers = this.handlers.filter(handler => handler.countryCodes.filter(countryCode => countryCode in ctx.config).length);
const { streams, ttl } = await this.streamResolver.resolve(ctx, handlers, type, id);
const { streams, ttl } = await this.streamResolver.resolve(ctx, handlers, type, ImdbId.fromString(id));
if (ttl && envIsProd()) {
res.setHeader('Cache-Control', `max-age=${ttl / 1000}, public`);

View file

@ -1,7 +1,7 @@
import winston from 'winston';
import { CineHDPlus } from './CineHDPlus';
import { ExtractorRegistry } from '../extractor';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,31 +12,26 @@ const handler = new CineHDPlus(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { es: 'on', mx: 'on' } };
describe('CineHDPlus', () => {
test('does not handle non imdb series', async () => {
const streams = await handler.handle(ctx, 'series', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent series gracefully', async () => {
const streams = await handler.handle(ctx, 'series', 'tt12345678:1:1');
const streams = await handler.handle(ctx, 'series', new ImdbId('tt12345678', 1, 1));
expect(streams).toHaveLength(0);
});
test('handle imdb black mirror s2e3 (mx)', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt2085059:2:3')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt2085059', 2, 3))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
test('handle imdb babylon 5 s2e3 (es)', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt0105946:2:3')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt0105946', 2, 3))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
test('does not return mx results for es and vice-versa', async () => {
const streamsEs = (await handler.handle({ ...ctx, ...{ config: { es: 'on' } } }, 'series', 'tt2085059:2:3')).filter(stream => stream !== undefined);
const streamsEs = (await handler.handle({ ...ctx, ...{ config: { es: 'on' } } }, 'series', new ImdbId('tt2085059', 2, 3))).filter(stream => stream !== undefined);
expect(streamsEs).toHaveLength(0);
const streamsMx = (await handler.handle({ ...ctx, ...{ config: { mx: 'on' } } }, 'series', 'tt0105946:2:3')).filter(stream => stream !== undefined);
const streamsMx = (await handler.handle({ ...ctx, ...{ config: { mx: 'on' } } }, 'series', new ImdbId('tt0105946', 2, 3))).filter(stream => stream !== undefined);
expect(streamsMx).toHaveLength(0);
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { Fetcher, ImdbId } from '../utils';
import { Fetcher, getImdbId, Id, ImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,8 @@ export class CineHDPlus implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
const imdbId = ImdbId.fromString(id);
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const seriesPageUrl = await this.fetchSeriesPageUrl(ctx, imdbId);
if (!seriesPageUrl) {

View file

@ -1,7 +1,7 @@
import winston from 'winston';
import { Eurostreaming } from './Eurostreaming';
import { ExtractorRegistry } from '../extractor';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new Eurostreaming(fetcher, new ExtractorRegistry(logger, fetcher
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { it: 'on' } };
describe('Eurostreaming', () => {
test('does not handle non imdb series', async () => {
const streams = await handler.handle(ctx, 'series', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent series gracefully', async () => {
const streams = await handler.handle(ctx, 'series', 'tt12345678:1:1');
const streams = await handler.handle(ctx, 'series', new ImdbId('tt12345678', 1, 1));
expect(streams).toHaveLength(0);
});
test('handle imdb black mirror s2e4', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt2085059:2:4')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt2085059', 2, 4))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { ImdbId, Fetcher } from '../utils';
import { ImdbId, Fetcher, Id, getImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,8 @@ export class Eurostreaming implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
const imdbId = ImdbId.fromString(id);
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const seriesPageUrl = await this.fetchSeriesPageUrl(ctx, imdbId);
if (!seriesPageUrl) {

View file

@ -1,7 +1,7 @@
import winston from 'winston';
import { Frembed } from './Frembed';
import { ExtractorRegistry } from '../extractor';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId, TmdbId } from '../utils';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new Frembed(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { fr: 'on' } };
describe('Frembed', () => {
test('does not handle non imdb and tmdb series', async () => {
const streams = await handler.handle(ctx, 'series', 'kitsu:123');
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);
const streams = (await handler.handle(ctx, 'series', new ImdbId('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 streams = (await handler.handle(ctx, 'series', new TmdbId(42009, 4, 2))).filter(stream => stream !== undefined);
const streamsWithoutTtl = streams.map((stream) => {
delete stream.ttl;

View file

@ -1,6 +1,6 @@
import { ContentType } from 'stremio-addon-sdk';
import { Handler } from './types';
import { Fetcher, getTmdbIdFromImdbId, ImdbId, TmdbId } from '../utils';
import { Fetcher, getTmdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -21,15 +21,8 @@ export class Frembed implements Handler {
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, ImdbId.fromString(id));
} else if (/^\d+:/.test(id)) {
tmdbId = TmdbId.fromString(id);
} else {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const apiUrl = new URL(`https://frembed.space/api/series?id=${tmdbId.id}&sa=${tmdbId.series}&epi=${tmdbId.episode}&idType=tmdb`);

View file

@ -1,6 +1,6 @@
import winston from 'winston';
import { FrenchCloud } from './FrenchCloud';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new FrenchCloud(fetcher, new ExtractorRegistry(logger, fetcher))
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { fr: 'on' } };
describe('FrenchCloud', () => {
test('does not handle non imdb movies', async () => {
const streams = await handler.handle(ctx, 'movie', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent movies gracefully', async () => {
const streams = await handler.handle(ctx, 'movie', 'tt12345678');
const streams = await handler.handle(ctx, 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handle imdb the devil\'s bath', async () => {
const streams = (await handler.handle(ctx, 'movie', 'tt29141112')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'movie', new ImdbId('tt29141112', undefined, undefined))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { Fetcher, ImdbId } from '../utils';
import { Fetcher, getImdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,10 @@ export class FrenchCloud implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://frenchcloud.cam/movie/${ImdbId.fromString(id).id}`);
const pageUrl = new URL(`https://frenchcloud.cam/movie/${imdbId.id}`);
const html = await this.fetcher.text(ctx, pageUrl);
const $ = cheerio.load(html);

View file

@ -1,7 +1,7 @@
import winston from 'winston';
import { KinoKiste } from './KinoKiste';
import { ExtractorRegistry } from '../extractor';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new KinoKiste(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { de: 'on' } };
describe('KinoKiste', () => {
test('does not handle non imdb series', async () => {
const streams = await handler.handle(ctx, 'series', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent series gracefully', async () => {
const streams = await handler.handle(ctx, 'series', 'tt12345678:1:1');
const streams = await handler.handle(ctx, 'series', new ImdbId('tt12345678', 1, 1));
expect(streams).toHaveLength(0);
});
test('handle imdb black mirror s2e4', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt2085059:2:4')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt2085059', 2, 4))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { ImdbId, Fetcher } from '../utils';
import { ImdbId, Fetcher, getImdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,8 @@ export class KinoKiste implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
const imdbId = ImdbId.fromString(id);
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const seriesPageUrl = await this.fetchSeriesPageUrl(ctx, imdbId);
if (!seriesPageUrl) {

View file

@ -1,6 +1,6 @@
import winston from 'winston';
import { MeineCloud } from './MeineCloud';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new MeineCloud(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { de: 'on' } };
describe('MeineCloud', () => {
test('does not handle non imdb movies', async () => {
const streams = await handler.handle(ctx, 'movie', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent movies gracefully', async () => {
const streams = await handler.handle(ctx, 'movie', 'tt12345678');
const streams = await handler.handle(ctx, 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handle imdb the devil\'s bath', async () => {
const streams = (await handler.handle(ctx, 'movie', 'tt29141112')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'movie', new ImdbId('tt29141112', undefined, undefined))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { Fetcher, ImdbId } from '../utils';
import { Fetcher, getImdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,10 @@ export class MeineCloud implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://meinecloud.click/movie/${ImdbId.fromString(id).id}`);
const pageUrl = new URL(`https://meinecloud.click/movie/${imdbId.id}`);
const html = await this.fetcher.text(ctx, pageUrl);
const $ = cheerio.load(html);

View file

@ -1,6 +1,6 @@
import winston from 'winston';
import { MostraGuarda } from './MostraGuarda';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new MostraGuarda(fetcher, new ExtractorRegistry(logger, fetcher)
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { it: 'on' } };
describe('MostraGuarda', () => {
test('does not handle non imdb movies', async () => {
const streams = await handler.handle(ctx, 'movie', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent movies gracefully', async () => {
const streams = await handler.handle(ctx, 'movie', 'tt12345678');
const streams = await handler.handle(ctx, 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handle imdb the devil\'s bath', async () => {
const streams = (await handler.handle(ctx, 'movie', 'tt29141112')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'movie', new ImdbId('tt29141112', undefined, undefined))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { Fetcher, ImdbId } from '../utils';
import { Fetcher, getImdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,10 @@ export class MostraGuarda implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://mostraguarda.stream/movie/${ImdbId.fromString(id).id}`);
const pageUrl = new URL(`https://mostraguarda.stream/movie/${imdbId.id}`);
const html = await this.fetcher.text(ctx, pageUrl);
const $ = cheerio.load(html);

View file

@ -1,6 +1,6 @@
import winston from 'winston';
import { Soaper } from './Soaper';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId, TmdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,33 +12,28 @@ 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');
const streams = await handler.handle(ctx, 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handle non-uploaded movie gracefully', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt29141112')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt29141112', undefined, undefined))).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);
const streams = (await handler.handle(ctx, 'series', new ImdbId('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);
const streams = (await handler.handle(ctx, 'series', new ImdbId('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 streams = (await handler.handle(ctx, 'series', new TmdbId(42009, 4, 2))).filter(stream => stream !== undefined);
const streamsWithoutTtl = streams.map((stream) => {
delete stream.ttl;
@ -49,7 +44,7 @@ describe('Soaper', () => {
});
test('handle imdb full metal jacket', async () => {
const streams = (await handler.handle(ctx, 'series', 'tt0093058')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'series', new ImdbId('tt0093058', undefined, undefined))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,14 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import {
Fetcher,
getTmdbIdFromImdbId,
getTmdbMovieDetails,
getTmdbTvDetails,
ImdbId,
TmdbId,
} from '../utils';
import { Fetcher, getTmdbId, getTmdbMovieDetails, getTmdbTvDetails, Id, TmdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -31,15 +24,8 @@ export class Soaper implements Handler {
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, ImdbId.fromString(id));
} else if (/^\d+:/.test(id)) {
tmdbId = TmdbId.fromString(id);
} else {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [keyword, year, hrefPrefix] = await this.getKeywordYearAndHrefPrefix(ctx, tmdbId);

View file

@ -1,6 +1,6 @@
import winston from 'winston';
import { VerHdLink } from './VerHdLink';
import { Fetcher } from '../utils';
import { Fetcher, ImdbId } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context } from '../types';
jest.mock('../utils/Fetcher');
@ -12,18 +12,13 @@ const handler = new VerHdLink(fetcher, new ExtractorRegistry(logger, fetcher));
const ctx: Context = { id: 'id', ip: '127.0.0.1', config: { es: 'on', mx: 'on' } };
describe('VerHdLink', () => {
test('does not handle non imdb movies', async () => {
const streams = await handler.handle(ctx, 'movie', 'kitsu:123');
expect(streams).toHaveLength(0);
});
test('handles non-existent movies gracefully', async () => {
const streams = await handler.handle(ctx, 'movie', 'tt12345678');
const streams = await handler.handle(ctx, 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handle titanic', async () => {
const streams = (await handler.handle(ctx, 'movie', 'tt0120338')).filter(stream => stream !== undefined);
const streams = (await handler.handle(ctx, 'movie', new ImdbId('tt0120338', undefined, undefined))).filter(stream => stream !== undefined);
expect(streams).toMatchSnapshot();
});
});

View file

@ -1,7 +1,7 @@
import { ContentType } from 'stremio-addon-sdk';
import * as cheerio from 'cheerio';
import { Handler } from './types';
import { Fetcher, ImdbId } from '../utils';
import { Fetcher, getImdbId, Id } from '../utils';
import { ExtractorRegistry } from '../extractor';
import { Context, CountryCode } from '../types';
@ -22,12 +22,10 @@ export class VerHdLink implements Handler {
this.extractorRegistry = extractorRegistry;
}
readonly handle = async (ctx: Context, _type: string, id: string) => {
if (!id.startsWith('tt')) {
return [];
}
readonly handle = async (ctx: Context, _type: string, id: Id) => {
const imdbId = await getImdbId(ctx, this.fetcher, id);
const pageUrl = new URL(`https://verhdlink.cam/movie/${ImdbId.fromString(id).id}`);
const pageUrl = new URL(`https://verhdlink.cam/movie/${imdbId.id}`);
const html = await this.fetcher.text(ctx, pageUrl);
const $ = cheerio.load(html);

View file

@ -1,5 +1,6 @@
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode, UrlResult } from '../types';
import { Id } from '../utils';
export interface Handler {
readonly id: string;
@ -10,5 +11,5 @@ export interface Handler {
readonly countryCodes: CountryCode[];
readonly handle: (ctx: Context, type: ContentType, id: string) => Promise<(UrlResult | undefined)[]>;
readonly handle: (ctx: Context, type: ContentType, id: Id) => Promise<(UrlResult | undefined)[]>;
}

View file

@ -14,7 +14,7 @@ import {
} from './handler';
import { ExtractorRegistry } from './extractor';
import { ConfigureController, ManifestController, StreamController } from './controller';
import { envGet, envIsProd, Fetcher, getImdbIdFromTmdbId, StreamResolver, tmdbFetch } from './utils';
import { envGet, envIsProd, Fetcher, StreamResolver, tmdbFetch, TmdbId } from './utils';
const logger = winston.createLogger({
transports: [
@ -95,8 +95,7 @@ const cacheWarmup = async () => {
}
});
for (const id of movieIds) {
const imdbId = await getImdbIdFromTmdbId(ctx, fetcher, { id, series: undefined, episode: undefined });
await streamResolver.resolve(ctx, handlers, 'movie', imdbId.id);
await streamResolver.resolve(ctx, handlers, 'movie', new TmdbId(id, undefined, undefined));
}
logger.info(`warmed up cache with ${movieIds.length} movies`, ctx);
@ -110,8 +109,7 @@ const cacheWarmup = async () => {
}
});
for (const id of tvShowIds) {
const imdbId = await getImdbIdFromTmdbId(ctx, fetcher, { id, series: 1, episode: 1 });
await streamResolver.resolve(ctx, handlers, 'series', `${imdbId.id}:1:1`);
await streamResolver.resolve(ctx, handlers, 'series', new TmdbId(id, 1, 1));
}
logger.info(`warmed up cache with ${tvShowIds.length} tv shows`, ctx);

View file

@ -6,6 +6,7 @@ import { Handler, MeineCloud, MostraGuarda } from '../handler';
import { Fetcher } from './Fetcher';
import { Context, CountryCode, TIMEOUT, UrlResult } from '../types';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error';
import { ImdbId } from './id';
jest.mock('../utils/Fetcher');
const logger = winston.createLogger({ transports: [new winston.transports.Console({ level: 'nope' })] });
@ -20,7 +21,7 @@ const mostraGuarda = new MostraGuarda(fetcher, extractorRegistry);
describe('resolve', () => {
test('returns info as stream if no handlers were configured', async () => {
const streams = await streamResolver.resolve(ctx, [], 'movie', 'tt123456789');
const streams = await streamResolver.resolve(ctx, [], 'movie', new ImdbId('tt123456789', undefined, undefined));
expect(streams).toMatchSnapshot();
});
@ -28,7 +29,7 @@ describe('resolve', () => {
test('returns handler errors as stream', async () => {
const fetcherSpy = jest.spyOn(fetcher, 'text').mockRejectedValue('ups, an error occurred.');
const streams = await streamResolver.resolve(ctx, [meineCloud], 'movie', 'tt123456789');
const streams = await streamResolver.resolve(ctx, [meineCloud], 'movie', new ImdbId('tt123456789', undefined, undefined));
expect(streams).toMatchSnapshot();
@ -36,19 +37,19 @@ describe('resolve', () => {
});
test('returns empty array if no handler found anything', async () => {
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt12345678');
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toMatchSnapshot();
});
test('returns empty array if no handler supported the type', async () => {
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'series', 'tt12345678:1:1');
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'series', new ImdbId('tt12345678', 1, 1));
expect(streams).toMatchSnapshot();
});
test('returns sorted results', async () => {
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', 'tt29141112');
const streams = await streamResolver.resolve(ctx, [meineCloud, mostraGuarda], 'movie', new ImdbId('tt29141112', undefined, undefined));
expect(streams).toMatchSnapshot();
});
@ -128,10 +129,10 @@ describe('resolve', () => {
};
}
const streams = await streamResolver.resolve(ctx, [new MockHandler()], 'movie', 'tt11655566');
const streams = await streamResolver.resolve(ctx, [new MockHandler()], 'movie', new ImdbId('tt11655566', undefined, undefined));
expect(streams).toMatchSnapshot();
const streamsWithoutExternalUrls = await streamResolver.resolve({ ...ctx, config: { ...ctx.config, excludeExternalUrls: 'on' } }, [new MockHandler()], 'movie', 'tt11655566');
const streamsWithoutExternalUrls = await streamResolver.resolve({ ...ctx, config: { ...ctx.config, excludeExternalUrls: 'on' } }, [new MockHandler()], 'movie', new ImdbId('tt11655566', undefined, undefined));
expect(streamsWithoutExternalUrls).toMatchSnapshot();
});
@ -144,7 +145,7 @@ describe('resolve', () => {
handle: jest.fn().mockRejectedValue(new NotFoundError()),
};
const streams = await streamResolver.resolve(ctx, [mockHandler], 'movie', 'tt12345678');
const streams = await streamResolver.resolve(ctx, [mockHandler], 'movie', new ImdbId('tt12345678', undefined, undefined));
expect(streams).toMatchSnapshot();
});

View file

@ -6,6 +6,7 @@ import { Handler } from '../handler';
import { BlockedError, HttpError, NotFoundError, QueueIsFullError } from '../error';
import { flagFromCountryCode, languageFromCountryCode } from './language';
import { envGetAppName } from './env';
import { Id } from './id';
interface ResolveResponse {
streams: Stream[];
@ -19,7 +20,7 @@ export class StreamResolver {
this.logger = logger;
}
readonly resolve = async (ctx: Context, handlers: Handler[], type: ContentType, id: string): Promise<ResolveResponse> => {
readonly resolve = async (ctx: Context, handlers: Handler[], type: ContentType, id: Id): Promise<ResolveResponse> => {
if (handlers.length === 0) {
return {
streams: [

View file

@ -1,2 +1,26 @@
import { ImdbId } from './ImdbId';
import { TmdbId } from './TmdbId';
import { getImdbIdFromTmdbId, getTmdbIdFromImdbId } from '../tmdb';
import { Fetcher } from '../Fetcher';
import { Context } from '../../types';
export * from './ImdbId';
export * from './TmdbId';
export type Id = ImdbId | TmdbId;
export const getImdbId = async (ctx: Context, fetcher: Fetcher, id: Id): Promise<ImdbId> => {
if (id instanceof TmdbId) {
return await getImdbIdFromTmdbId(ctx, fetcher, id);
}
return id;
};
export const getTmdbId = async (ctx: Context, fetcher: Fetcher, id: Id): Promise<TmdbId> => {
if (id instanceof ImdbId) {
return await getTmdbIdFromImdbId(ctx, fetcher, id);
}
return id;
};