feat(source): add HomeCine

This commit is contained in:
WebStreamr 2025-07-18 17:55:03 +00:00
parent 533470d080
commit 5759a311aa
No known key found for this signature in database
14 changed files with 5274 additions and 1 deletions

View file

@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
import winston from 'winston';
import { ConfigureController, ManifestController, StreamController } from './controller';
import { createExtractors, ExtractorRegistry } from './extractor';
import { CineHDPlus, Cuevana, Eurostreaming, Frembed, FrenchCloud, KinoGer, MegaKino, MeineCloud, MostraGuarda, Movix, PrimeWire, Soaper, Source, StreamKiste, VerHdLink, VidSrc, VixSrc } from './source';
import { CineHDPlus, Cuevana, Eurostreaming, Frembed, FrenchCloud, HomeCine, KinoGer, MegaKino, MeineCloud, MostraGuarda, Movix, PrimeWire, Soaper, Source, StreamKiste, VerHdLink, VidSrc, VixSrc } from './source';
import { contextFromRequest, envGet, envIsProd, Fetcher, StreamResolver } from './utils';
const logger = winston.createLogger({
@ -39,6 +39,7 @@ const sources: Source[] = [
// ES / MX
new CineHDPlus(fetcher),
new Cuevana(fetcher),
new HomeCine(fetcher),
new VerHdLink(fetcher),
// DE
new KinoGer(fetcher),

View file

@ -0,0 +1,43 @@
import { createTestContext } from '../test';
import { FetcherMock, TmdbId } from '../utils';
import { HomeCine } from './HomeCine';
const ctx = createTestContext({ es: 'on', mx: 'on' });
describe('HomeCine', () => {
let handler: HomeCine;
beforeEach(() => {
handler = new HomeCine(new FetcherMock(`${__dirname}/__fixtures__/HomeCine`));
});
test('handles non-uploaded movie gracefully', async () => {
const streams = await handler.handle(ctx, 'series', new TmdbId(3176, undefined, undefined));
expect(streams).toHaveLength(0);
});
test('handles non-existent episodes gracefully', async () => {
const streams = await handler.handle(ctx, 'series', new TmdbId(1402, 1, 99));
expect(streams).toHaveLength(0);
});
test('handle walking dead s1e1', async () => {
const streams = await handler.handle(ctx, 'series', new TmdbId(1402, 1, 1));
expect(streams).toMatchSnapshot();
});
test('handle el camino', async () => {
const streams = await handler.handle(ctx, 'movie', new TmdbId(559969, undefined, undefined));
expect(streams).toMatchSnapshot();
});
test('does not return mx content for es', async () => {
const streams = await handler.handle(createTestContext({ es: 'on' }), 'movie', new TmdbId(559969, undefined, undefined));
expect(streams).toMatchSnapshot();
});
test('does not return es content for mx', async () => {
const streams = await handler.handle(createTestContext({ mx: 'on' }), 'movie', new TmdbId(559969, undefined, undefined));
expect(streams).toMatchSnapshot();
});
});

91
src/source/HomeCine.ts Normal file
View file

@ -0,0 +1,91 @@
import * as cheerio from 'cheerio';
import { ContentType } from 'stremio-addon-sdk';
import { Context, CountryCode } from '../types';
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id, TmdbId } from '../utils';
import { Source, SourceResult } from './types';
export class HomeCine implements Source {
public readonly id = 'homecine';
public readonly label = 'HomeCine';
public readonly contentTypes: ContentType[] = ['movie', 'series'];
public readonly countryCodes: CountryCode[] = [CountryCode.es, CountryCode.mx];
private readonly baseUrl = 'https://www3.homecine.to';
private readonly fetcher: Fetcher;
public constructor(fetcher: Fetcher) {
this.fetcher = fetcher;
}
public async handle(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
const [name, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId, 'es');
const pageUrl = await this.fetchPageUrl(ctx, name, year, tmdbId);
if (!pageUrl) {
return [];
}
const pageHtml = await this.fetcher.text(ctx, pageUrl);
let linksHtml = pageHtml;
if (tmdbId.season) {
const episodeUrl = await this.fetchEpisodeUrl(pageHtml, tmdbId);
if (!episodeUrl) {
return [];
}
linksHtml = await this.fetcher.text(ctx, episodeUrl);
}
const title = tmdbId.season ? `${name} ${tmdbId.season}x${tmdbId.episode}` : `${name} (${year})`;
const $ = cheerio.load(linksHtml);
return $('.les-content a')
.map((_i, el) => {
let countryCode: CountryCode;
if ($(el).text().toLowerCase().includes('latino') && CountryCode.mx in ctx.config) {
countryCode = CountryCode.mx;
} else if ($(el).text().toLowerCase().includes('castellano') && CountryCode.es in ctx.config) {
countryCode = CountryCode.es;
} else {
return [];
}
return { countryCode, title, url: new URL($('iframe', $(el).attr('href')).attr('src') as string) };
}).toArray();
};
private readonly fetchPageUrl = async (ctx: Context, keyword: string, year: number, tmdbId: TmdbId): Promise<URL | undefined> => {
const searchUrl = new URL(`/?s=${encodeURIComponent(keyword)}`, this.baseUrl);
const html = await this.fetcher.text(ctx, searchUrl);
const $ = cheerio.load(html);
const urls = $(`a[oldtitle="${keyword} (${year})"], a[oldtitle="${keyword}"]`)
.filter((_i, el) => $(el).siblings('#hidden_tip').find(`a[href$="release-year/${year}"]`).length !== 0)
.map((_i, el) => new URL($(el).attr('href') as string))
.toArray()
.filter(url => tmdbId.season ? url.href.includes('/series/') : !url.href.includes('/series/'));
return urls[0];
};
private readonly fetchEpisodeUrl = async (pageHtml: string, tmdbId: TmdbId): Promise<URL | undefined> => {
const $ = cheerio.load(pageHtml);
const urls = $('#seasons a')
.map((_i, el) => new URL($(el).attr('href') as string))
.toArray()
.filter(url => url.href.endsWith(`-temporada-${tmdbId.season}-capitulo-${tmdbId.episode}`));
return urls[0];
};
}

View file

@ -0,0 +1 @@
{"adult":false,"backdrop_path":"/amBvmIshdsSkOtvVIgxl7YSQ9Dg.jpg","belongs_to_collection":{"id":16302,"name":"Battle Royale - Colección","poster_path":"/e9x8MueDBGtEAALdZZIZQ96i8C6.jpg","backdrop_path":"/gKpdj5aKtxrAoAbJJ3EtBZHcsyP.jpg"},"budget":4500000,"genres":[{"id":18,"name":"Drama"},{"id":53,"name":"Suspense"},{"id":28,"name":"Acción"}],"homepage":"","id":3176,"imdb_id":"tt0266308","origin_country":["JP"],"original_language":"ja","original_title":"バトル・ロワイアル","overview":"En el amanecer de un nuevo milenio, el país está al borde del colapso. Millones de personas vagan sin empleo. La violencia en la escuela está descontrolada y adolescentes rebeldes protagonizan boicots masivos. El gobierno contrataca con “Battle Royale”. Cada año, una clase es escogida al azar para que se enfrente, en una isla abandonada, a un cruel juego de supervivencia.","popularity":4.1635,"poster_path":"/e9UQGoX2ZFNdrjLwEejMSMDSxmR.jpg","production_companies":[{"id":5822,"logo_path":"/qyTbRgCyU9NLKvKaiQVbadtr7RY.png","name":"Toei Company","origin_country":"JP"},{"id":157807,"logo_path":"/tyrJOOVAUIxQEJzjP1PhP2H5zFi.png","name":"WOWOW","origin_country":"JP"},{"id":11538,"logo_path":null,"name":"AM Associates","origin_country":"JP"},{"id":11539,"logo_path":null,"name":"Kobi","origin_country":"JP"},{"id":3032,"logo_path":"/gaf3ez34cuvG9snphWGreB1WerA.png","name":"Nippan Group Holdings","origin_country":"JP"},{"id":11540,"logo_path":null,"name":"MF Pictures","origin_country":"JP"},{"id":3656,"logo_path":"/mY6L0bHRp8fNWfICPqLjxp3khvq.png","name":"GAGA Communications","origin_country":"JP"}],"production_countries":[{"iso_3166_1":"JP","name":"Japan"}],"release_date":"2000-12-16","revenue":30600000,"runtime":114,"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"},{"english_name":"Japanese","iso_639_1":"ja","name":"日本語"}],"status":"Released","tagline":"¿Puedes matar a tu mejor amigo?","title":"Battle Royale","video":false,"vote_average":7.284,"vote_count":3539}

View file

@ -0,0 +1 @@
{"adult":false,"backdrop_path":"/uLXK1LQM28XovWHPao3ViTeggXA.jpg","belongs_to_collection":null,"budget":0,"genres":[{"id":80,"name":"Crimen"},{"id":18,"name":"Drama"},{"id":53,"name":"Suspense"}],"homepage":"https://www.netflix.com/es/title/81078819","id":559969,"imdb_id":"tt9243946","origin_country":["US"],"original_language":"en","original_title":"El Camino: A Breaking Bad Movie","overview":"Tiempo después de los eventos sucedidos tras el último episodio de la serie \"Breaking Bad\", el fugitivo Jesse Pinkman (Aaron Paul) huye de sus perseguidores, de la ley y de su pasado.","popularity":6.0483,"poster_path":"/hoWADuvXs3Ua4AXBAiZYnppTupO.jpg","production_companies":[{"id":11073,"logo_path":"/aCbASRcI1MI7DXjPbSW9Fcv9uGR.png","name":"Sony Pictures Television","origin_country":"US"},{"id":2605,"logo_path":null,"name":"Gran Via Productions","origin_country":"US"},{"id":33742,"logo_path":"/2jdh2sEa0R6y6uT0F7g0IgA2WO8.png","name":"High Bridge Productions","origin_country":"US"}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"release_date":"2019-10-11","revenue":0,"runtime":123,"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"}],"status":"Released","tagline":"","title":"El Camino: Una película de Breaking Bad","video":false,"vote_average":6.965,"vote_count":5113}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
exports[`HomeCine does not return es content for mx 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad (2019)",
"url": "https://fastream.to/embed-kc5dvcy34x8p.html",
},
]
`;
exports[`HomeCine does not return mx content for es 1`] = `
[
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad (2019)",
"url": "https://fastream.to/embed-c9tmfzsbfj0z.html",
},
]
`;
exports[`HomeCine handle el camino 1`] = `
[
{
"countryCode": "mx",
"title": "El Camino: Una película de Breaking Bad (2019)",
"url": "https://fastream.to/embed-kc5dvcy34x8p.html",
},
{
"countryCode": "es",
"title": "El Camino: Una película de Breaking Bad (2019)",
"url": "https://fastream.to/embed-c9tmfzsbfj0z.html",
},
]
`;
exports[`HomeCine handle walking dead s1e1 1`] = `
[
{
"countryCode": "mx",
"title": "The Walking Dead 1x1",
"url": "https://fastream.to/embed-nkztf1memuca.html",
},
{
"countryCode": "es",
"title": "The Walking Dead 1x1",
"url": "https://fastream.to/embed-5bv96pdifz0b.html",
},
]
`;

View file

@ -3,6 +3,7 @@ export * from './Cuevana';
export * from './Eurostreaming';
export * from './Frembed';
export * from './FrenchCloud';
export * from './HomeCine';
export * from './KinoGer';
export * from './MegaKino';
export * from './MeineCloud';