feat: add Albanian source Kokoshka #430
20 changed files with 260 additions and 2 deletions
33
src/source/Kokoshka.test.ts
Normal file
33
src/source/Kokoshka.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { createTestContext } from '../test';
|
||||
import { FetcherMock, TmdbId } from '../utils';
|
||||
import { Kokoshka } from './Kokoshka';
|
||||
|
||||
const ctx = createTestContext({ al: 'on' });
|
||||
|
||||
describe('Kokoshka', () => {
|
||||
let source: Kokoshka;
|
||||
|
||||
beforeEach(() => {
|
||||
source = new Kokoshka(new FetcherMock(`${__dirname}/__fixtures__/Kokoshka`));
|
||||
});
|
||||
|
||||
test('handle non-existent content gracefully', async () => {
|
||||
const streams = await source.handle(ctx, 'series', new TmdbId(287877, 1, 1));
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handle non-existent episode gracefully', async () => {
|
||||
const streams = await source.handle(ctx, 'series', new TmdbId(286801, 1, 99));
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handle Weapons 2025', async () => {
|
||||
const streams = await source.handle(ctx, 'movie', new TmdbId(1078605, undefined, undefined));
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handle Monster: The Ed Gein Story 2025', async () => {
|
||||
const streams = await source.handle(ctx, 'series', new TmdbId(286801, 1, 3));
|
||||
expect(streams).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
97
src/source/Kokoshka.ts
Normal file
97
src/source/Kokoshka.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import * as cheerio from 'cheerio';
|
||||
|
No me neither got an direct source link but only embed one so this means the second line is a dead code and must be removed... No me neither got an direct source link but only embed one so this means the second line is a dead code and must be removed...
|
||||
import { ContentType } from 'stremio-addon-sdk';
|
||||
import { Context, CountryCode } from '../types';
|
||||
import { Fetcher, getTmdbId, getTmdbNameAndYear, Id, TmdbId } from '../utils';
|
||||
import { Source, SourceResult } from './Source';
|
||||
|
||||
interface DooplayerResponse {
|
||||
embed_url: string | null;
|
||||
type: string | false;
|
||||
}
|
||||
|
||||
export class Kokoshka extends Source {
|
||||
public readonly id = 'kokoshka';
|
||||
|
||||
public readonly label = 'Kokoshka';
|
||||
|
||||
public readonly contentTypes: ContentType[] = ['movie', 'series'];
|
||||
|
||||
public readonly countryCodes: CountryCode[] = [CountryCode.al];
|
||||
|
||||
public readonly baseUrl = 'https://kokoshka.digital';
|
||||
|
||||
private readonly fetcher: Fetcher;
|
||||
|
||||
public constructor(fetcher: Fetcher) {
|
||||
super();
|
||||
|
||||
this.fetcher = fetcher;
|
||||
}
|
||||
|
||||
public async handleInternal(ctx: Context, _type: string, id: Id): Promise<SourceResult[]> {
|
||||
const tmdbId = await getTmdbId(ctx, this.fetcher, id);
|
||||
|
||||
let pageUrl = await this.fetchPageUrl(ctx, tmdbId);
|
||||
if (!pageUrl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (tmdbId.season) {
|
||||
pageUrl = await this.fetchEpisodeUrl(ctx, pageUrl, tmdbId);
|
||||
if (!pageUrl) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const pageHtml = await this.fetcher.text(ctx, pageUrl);
|
||||
|
||||
const $ = cheerio.load(pageHtml);
|
||||
|
||||
const title = $('title').first().text().trim();
|
||||
|
||||
return Promise.all(
|
||||
$('.dooplay_player_option:not(#player-option-trailer)')
|
||||
.map(async (_i, el) => {
|
||||
const post = parseInt($(el).attr('data-post') as string);
|
||||
const type = $(el).attr('data-type') as string;
|
||||
const nume = parseInt($(el).attr('data-nume') as string);
|
||||
|
||||
const dooplayerUrl = new URL(`/wp-json/dooplayer/v2/${post}/${type}/${nume}`, this.baseUrl);
|
||||
const dooplayerResponse = await this.fetcher.json(ctx, dooplayerUrl, { headers: { Referer: pageUrl.href } }) as DooplayerResponse;
|
||||
|
||||
return {
|
||||
url: new URL(dooplayerResponse.embed_url as string),
|
||||
meta: {
|
||||
countryCodes: [CountryCode.al],
|
||||
referer: pageUrl.href,
|
||||
title,
|
||||
},
|
||||
};
|
||||
})
|
||||
.toArray(),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly fetchPageUrl = async (ctx: Context, tmdbId: TmdbId): Promise<URL | undefined> => {
|
||||
const [name, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId);
|
||||
|
||||
const searchUrl = new URL(`/?s=${encodeURIComponent(`${name.replace(':', '')} ${year}`)}`, this.baseUrl);
|
||||
const html = await this.fetcher.text(ctx, searchUrl);
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
return $(`.result-item:has(${tmdbId.season ? '.tvshows' : '.movies'}) a`)
|
||||
.map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl))
|
||||
.get(0);
|
||||
};
|
||||
|
||||
private async fetchEpisodeUrl(ctx: Context, pageUrl: URL, tmdbId: TmdbId): Promise<URL | undefined> {
|
||||
const html = await this.fetcher.text(ctx, pageUrl);
|
||||
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
return $(`.episodiotitle a[href*="${tmdbId.season}x${tmdbId.episode}"]`)
|
||||
.map((_i, el) => new URL($(el).attr('href') as string, this.baseUrl))
|
||||
.get(0);
|
||||
}
|
||||
}
|
||||
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"adult":false,"backdrop_path":"/Q2OajDi2kcO6yErb1IAyVDTKMs.jpg","belongs_to_collection":{"id":1531466,"name":"Weapons Collection","poster_path":"/xoeSn325CFQLt1WC9jSBBTsvPWR.jpg","backdrop_path":"/xt2V8A0NVw4lUIUU8gRLnrCBrIR.jpg"},"budget":38000000,"genres":[{"id":27,"name":"Horror"},{"id":9648,"name":"Mystery"}],"homepage":"https://www.warnerbros.com/movies/weapons","id":1078605,"imdb_id":"tt26581740","origin_country":["US"],"original_language":"en","original_title":"Weapons","overview":"When all but one child from the same class mysteriously vanish on the same night at exactly the same time, a community is left questioning who or what is behind their disappearance.","popularity":99.0392,"poster_path":"/cpf7vsRZ0MYRQcnLWteD5jK9ymT.jpg","production_companies":[{"id":12,"logo_path":"/2ycs64eqV5rqKYHyQK0GVoKGvfX.png","name":"New Line Cinema","origin_country":"US"},{"id":240675,"logo_path":null,"name":"Subconscious","origin_country":"US"},{"id":829,"logo_path":"/iJo9zP5QCKyBsmKSOcvXd6UDNih.png","name":"Vertigo Entertainment","origin_country":"US"},{"id":18609,"logo_path":"/pxtYCfl1DWzvizqZcYwE4NefOOF.png","name":"BoulderLight Pictures","origin_country":"US"},{"id":216687,"logo_path":"/kKVYqekveOvLK1IgqdJojLjQvtu.png","name":"Domain Entertainment","origin_country":"US"}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"release_date":"2025-08-04","revenue":265894217,"runtime":129,"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"},{"english_name":"Spanish","iso_639_1":"es","name":"Español"}],"status":"Released","tagline":"Last night at 2:17 AM, every child from Mrs. Gandy's class woke up, got out of bed, went downstairs, opened the front door, walked into the dark ...and they never came back.","title":"Weapons","video":false,"vote_average":7.392,"vote_count":1887}
|
||||
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"adult":false,"backdrop_path":"/cm2oUAPiTE1ERoYYOzzgloQw4YZ.jpg","created_by":[{"id":59595,"credit_id":"67db0c885ef4639ebae77c84","name":"Ian Brennan","original_name":"Ian Brennan","gender":2,"profile_path":null}],"episode_run_time":[],"first_air_date":"2025-10-03","genres":[{"id":18,"name":"Drama"},{"id":80,"name":"Crime"}],"homepage":"https://www.netflix.com/title/81783093","id":286801,"in_production":false,"languages":["en","de"],"last_air_date":"2025-10-03","last_episode_to_air":{"id":6485709,"name":"The Godfather","overview":"Ed finds new purpose helping investigators crack a disturbing case. But as the past and present blur, his terrifying legacy looms larger than ever.","vote_average":7.6,"vote_count":6,"air_date":"2025-10-03","episode_number":8,"episode_type":"finale","production_code":"","runtime":66,"season_number":1,"show_id":286801,"still_path":"/buEEJshPhyxc8WzJCGCkcw81AUP.jpg"},"name":"Monster: The Ed Gein Story","next_episode_to_air":null,"networks":[{"id":213,"logo_path":"/wwemzKWzjKYJFfCeiB57q3r4Bcm.png","name":"Netflix","origin_country":""}],"number_of_episodes":8,"number_of_seasons":1,"origin_country":["US"],"original_language":"en","original_name":"Monster: The Ed Gein Story","overview":"The shocking true-life tale of Ed Gein, the infamous murderer and grave robber who inspired many of Hollywood's most iconic on-screen killers.","popularity":266.8154,"poster_path":"/iDHzRALtZCzHVmx7uyjTTKvMAPB.jpg","production_companies":[{"id":19328,"logo_path":"/2AdkrU1av4fDGw0Zaf68XH4YcqC.png","name":"Ryan Murphy Television","origin_country":"US"},{"id":182831,"logo_path":null,"name":"Prospect Films","origin_country":"US"}],"production_countries":[{"iso_3166_1":"US","name":"United States of America"}],"seasons":[{"air_date":"2025-10-03","episode_count":8,"id":447082,"name":"Miniseries","overview":"Serial killer. Grave robber. Psycho. In the frozen fields of 1950s rural Wisconsin, a friendly, mild-mannered recluse named Eddie Gein lived quietly on a decaying farm - hiding a house of horrors so gruesome it would redefine the American nightmare. Driven by isolation, psychosis, and an all-consuming obsession with his mother, Gein's perverse crimes birthed a new kind of monster that would haunt Hollywood for decades. From Psycho to The Texas Chainsaw Massacre to The Silence of the Lambs, Gein's macabre legacy gave birth to fictional monsters born in his image and ignited a cultural obsession with the criminally deviant. Ed Gein didn't just influence a genre -- he became the blueprint for modern horror.","poster_path":"/9oEJvxWAKvHUQGL8CzPwmfIxBGW.jpg","season_number":1,"vote_average":7.1}],"spoken_languages":[{"english_name":"English","iso_639_1":"en","name":"English"},{"english_name":"German","iso_639_1":"de","name":"Deutsch"}],"status":"Ended","tagline":"Before The Texas Chainsaw Massacre... there was Ed.","type":"Miniseries","vote_average":7.488,"vote_count":214}
|
||||
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"adult":false,"backdrop_path":"/olKSgSZVyDjqd11Bgv1lMk36Jm7.jpg","created_by":[{"id":5476237,"credit_id":"683c0c2b6f2cd45f867ba527","name":"Judith Westermann","original_name":"Judith Westermann","gender":0,"profile_path":null}],"episode_run_time":[],"first_air_date":"2025-05-09","genres":[{"id":35,"name":"Comedy"}],"homepage":"","id":287877,"in_production":true,"languages":["de"],"last_air_date":"2025-06-27","last_episode_to_air":{"id":6218896,"name":"Episode 8","overview":"","vote_average":0.0,"vote_count":0,"air_date":"2025-06-27","episode_number":8,"episode_type":"standard","production_code":"","runtime":29,"season_number":1,"show_id":287877,"still_path":null},"name":"Drunter & Drüber - Chaos auf dem Friedhof","next_episode_to_air":null,"networks":[{"id":189,"logo_path":"/k1poBlW9HGDnIyZU67BtH3BXQuz.png","name":"ORF 1","origin_country":"AT"},{"id":1024,"logo_path":"/w7HfLNm9CWwRmAMU58udl2L7We7.png","name":"Prime Video","origin_country":""}],"number_of_episodes":8,"number_of_seasons":1,"origin_country":["AT"],"original_language":"de","original_name":"Drunter & Drüber - Chaos auf dem Friedhof","overview":"When a rotten gravestone kills the cemetery manager, his deputy Butz Bohrlich already sees himself as the new number one at Donnerscheidt Cemetery. However, the ambitious man is ignored and the new boss Ursula Fink is put in front of him.","popularity":0.892,"poster_path":"/kIS1dc4PhVA9qDYIkPLytqzKUSh.jpg","production_companies":[{"id":220619,"logo_path":"/5g2txBSd5ZvRcwKNKy99RX00JYc.png","name":"RUNDFILM","origin_country":"AT"}],"production_countries":[{"iso_3166_1":"AT","name":"Austria"}],"seasons":[{"air_date":"2025-05-09","episode_count":8,"id":449135,"name":"Season 1","overview":"","poster_path":"/kIS1dc4PhVA9qDYIkPLytqzKUSh.jpg","season_number":1,"vote_average":0.0}],"spoken_languages":[{"english_name":"German","iso_639_1":"de","name":"Deutsch"}],"status":"Returning Series","tagline":"","type":"Scripted","vote_average":6.0,"vote_count":7}
|
||||
File diff suppressed because one or more lines are too long
12
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip
generated
Normal file
12
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip
generated
Normal file
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
7
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025
generated
Normal file
7
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025
generated
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"embed_url":"https:\/\/chuckle-tube.com\/e\/bfqvmxliwiaj","type":"iframe"}
|
||||
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"embed_url":"https:\/\/d-s.io\/e\/qjoay72ylazr","type":"iframe"}
|
||||
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"embed_url":"https:\/\/chuckle-tube.com\/e\/iji4pltez4eq","type":"iframe"}
|
||||
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2
generated
Normal file
1
src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2
generated
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"embed_url":"https:\/\/dsvplay.com\/e\/kle8q5fb816s","type":"iframe"}
|
||||
55
src/source/__snapshots__/Kokoshka.test.ts.snap
Normal file
55
src/source/__snapshots__/Kokoshka.test.ts.snap
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`Kokoshka handle Monster: The Ed Gein Story 2025 1`] = `
|
||||
[
|
||||
{
|
||||
"meta": {
|
||||
"countryCodes": [
|
||||
"al",
|
||||
],
|
||||
"referer": "https://kokoshka.digital/episodi/monster-the-ed-gein-story-1x3-me-titra-shqip/",
|
||||
"title": "Monster: The Ed Gein Story: 1x3 me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip",
|
||||
},
|
||||
"url": "https://chuckle-tube.com/e/iji4pltez4eq",
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"countryCodes": [
|
||||
"al",
|
||||
],
|
||||
"referer": "https://kokoshka.digital/episodi/monster-the-ed-gein-story-1x3-me-titra-shqip/",
|
||||
"title": "Monster: The Ed Gein Story: 1x3 me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip",
|
||||
},
|
||||
"url": "https://dsvplay.com/e/kle8q5fb816s",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Kokoshka handle Weapons 2025 1`] = `
|
||||
[
|
||||
{
|
||||
"meta": {
|
||||
"countryCodes": [
|
||||
"al",
|
||||
],
|
||||
"referer": "https://kokoshka.digital/filma/weapons-2025-me-titra-shqip/",
|
||||
"title": "Weapons (2025) me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip",
|
||||
},
|
||||
"url": "https://chuckle-tube.com/e/bfqvmxliwiaj",
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"countryCodes": [
|
||||
"al",
|
||||
],
|
||||
"referer": "https://kokoshka.digital/filma/weapons-2025-me-titra-shqip/",
|
||||
"title": "Weapons (2025) me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip",
|
||||
},
|
||||
"url": "https://d-s.io/e/qjoay72ylazr",
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Kokoshka handle non-existent content gracefully 1`] = `[]`;
|
||||
|
||||
exports[`Kokoshka handle non-existent episode gracefully 1`] = `[]`;
|
||||
|
|
@ -8,6 +8,7 @@ import { Frembed } from './Frembed';
|
|||
import { FrenchCloud } from './FrenchCloud';
|
||||
import { HomeCine } from './HomeCine';
|
||||
import { KinoGer } from './KinoGer';
|
||||
import { Kokoshka } from './Kokoshka';
|
||||
import { MegaKino } from './MegaKino';
|
||||
import { MeineCloud } from './MeineCloud';
|
||||
import { MostraGuarda } from './MostraGuarda';
|
||||
|
|
@ -27,6 +28,8 @@ export const createSources = (fetcher: Fetcher): Source[] => {
|
|||
// multi
|
||||
new FourKHDHub(fetcher),
|
||||
new VixSrc(fetcher),
|
||||
// AL
|
||||
new Kokoshka(fetcher),
|
||||
// EN
|
||||
new VidSrc(fetcher),
|
||||
// ES / MX
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export type Config = Partial<Record<CountryCode | 'showErrors' | 'includeExterna
|
|||
|
||||
export enum CountryCode {
|
||||
multi = 'multi',
|
||||
al = 'al',
|
||||
ar = 'ar',
|
||||
bg = 'bg',
|
||||
cs = 'cs',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,11 @@ exports[`buildManifest default manifest 1`] = `
|
|||
"title": "Multi 🌐 (4KHDHub, VixSrc)",
|
||||
"type": "checkbox",
|
||||
},
|
||||
{
|
||||
"key": "al",
|
||||
"title": "Albanian 🇦🇱 (Kokoshka)",
|
||||
"type": "checkbox",
|
||||
},
|
||||
{
|
||||
"key": "de",
|
||||
"title": "German 🇩🇪 (Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste)",
|
||||
|
|
@ -80,9 +85,9 @@ exports[`buildManifest default manifest 1`] = `
|
|||
],
|
||||
"description": "Provides HTTP URLs from streaming websites. Configure add-on for additional languages. Add MediaFlow proxy for more URLs.
|
||||
|
||||
Supported languages: German, English, Castilian Spanish, French, Italian, Latin American Spanish
|
||||
Supported languages: Albanian, German, English, Castilian Spanish, French, Italian, Latin American Spanish
|
||||
|
||||
Supported sources: 4KHDHub, VixSrc, VidSrc, CineHDPlus, Cuevana, HomeCine, VerHdLink, Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste, Frembed, FrenchCloud, Movix, Eurostreaming, MostraGuarda
|
||||
Supported sources: 4KHDHub, VixSrc, Kokoshka, VidSrc, CineHDPlus, Cuevana, HomeCine, VerHdLink, Einschalten, KinoGer, MegaKino, MeineCloud, StreamKiste, Frembed, FrenchCloud, Movix, Eurostreaming, MostraGuarda
|
||||
|
||||
Supported extractors: ",
|
||||
"id": "webstreamr",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { CountryCode } from '../types';
|
|||
|
||||
const countryCodeMap: Record<CountryCode, { language: string; flag: string; iso639: string | undefined }> = {
|
||||
multi: { language: 'Multi', flag: '🌐', iso639: undefined },
|
||||
al: { language: 'Albanian', flag: '🇦🇱', iso639: 'alb' },
|
||||
ar: { language: 'Arabic', flag: '🇸🇦', iso639: 'ara' },
|
||||
bg: { language: 'Bulgarian', flag: '🇧🇬', iso639: 'bul' },
|
||||
cs: { language: 'Czech', flag: '🇨🇿', iso639: 'ces' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue
@GLlgGL do you remember where you saw the endpoint returning sources? so far I only ever encountered
embed_url