From 79b2beac70cc7006ab834879b369df64066500c0 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:36:55 +0200 Subject: [PATCH 1/9] Create Kokoshka.ts --- src/source/Kokoshka.ts | 142 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/source/Kokoshka.ts diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts new file mode 100644 index 0000000..f40b28e --- /dev/null +++ b/src/source/Kokoshka.ts @@ -0,0 +1,142 @@ +import { ContentType } from 'stremio-addon-sdk'; +import { Context, CountryCode } from '../types'; +import { Fetcher, Id } from '../utils'; +import { Source, SourceResult } from './Source'; +import { getTmdbIdFromImdbId, getTmdbNameAndYear } from '../utils/tmdb'; +import { TmdbId } from '../utils/id'; + +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, imdbId: Id): Promise { + try { + // 1. IMDb → TMDb + let tmdbId: TmdbId; + if (type === 'series') { + const imdbBaseId = String(imdbId.id).split(':')[0]; + tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, { + id: imdbBaseId, + season: imdbId.season, + episode: imdbId.episode + } as any); + if (!tmdbId.season || !tmdbId.episode) return []; + } else { + tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, { id: imdbId } as any); + } + + // 2. Title + year + const [title, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId); + + // Helper: check if a URL returns a valid page (not 404) + const testUrl = async (ctx: Context, url: URL): Promise => { + try { + const html = await this.fetcher.text(ctx, url); + // crude but effective check for a dead embed page + return !/not\s+found/i.test(html) && html.length > 100; + } catch { + return false; + } + }; + + if (type === 'series') { + const cleanTitle = title + .toLowerCase() + .replace(/[:]/g, '') + .replace(/\s+/g, '-') + .replace(/[^\w-]/g, ''); + + const episodeUrl = new URL( + `/episodi/${cleanTitle}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, + this.baseUrl + ); + + const episodeHtml = await this.fetcher.text(ctx, episodeUrl); + const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); + if (!postIdMatch) return []; + const postId = postIdMatch[1]; + + // Try both tv servers + for (const server of [1, 2]) { + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); + const json = await this.fetcher.json(ctx, ajaxUrl) as { embed_url?: string }; + if (!json?.embed_url) continue; + + const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); + if (!embedId) continue; + + const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); + + if (await testUrl(ctx, finalUrl)) { + return [ + { + url: finalUrl, + meta: { + countryCodes: [CountryCode.al], + referer: this.baseUrl, + title: `${title} S${tmdbId.season}E${tmdbId.episode}`, + }, + }, + ]; + } + } + + return []; + } else { + // Movie + const cleanTitle = title.replace(/[:]/g, ''); + const query = encodeURIComponent(`${cleanTitle} ${year}`); + const searchUrl = new URL(`/?s=${query}`, this.baseUrl); + const searchHtml = await this.fetcher.text(ctx, searchUrl); + + const linkMatch = searchHtml.match(/class=["']title["'][^>]*>]+data-post=['"](\d+)['"]/); + if (!postIdMatch) return []; + const postId = postIdMatch[1]; + + // Try both movie servers + for (const server of [1, 2]) { + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); + const json = await this.fetcher.json(ctx, ajaxUrl) as { embed_url?: string }; + if (!json?.embed_url) continue; + + const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); + if (!embedId) continue; + + const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); + if (await testUrl(ctx, finalUrl)) { + return [ + { + url: finalUrl, + meta: { + countryCodes: [CountryCode.al], + referer: this.baseUrl, + title, + }, + }, + ]; + } + } + + return []; + } + } catch (err) { + console.error('Kokoshka handleInternal error:', err); + return []; + } + } +} -- 2.45.2 From 1f4f3fbd9ee19963f3308f3c8eccb8a44206a4da Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:40:57 +0200 Subject: [PATCH 2/9] Update types.ts Add Albanian code as country code --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index 343683f..c2421c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,6 +21,7 @@ export type Config = Partial Date: Tue, 14 Oct 2025 21:44:17 +0200 Subject: [PATCH 3/9] Update language.ts Add Albanian countrycode map --- src/utils/language.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/language.ts b/src/utils/language.ts index 26e8b31..93ef70c 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -2,6 +2,7 @@ import { CountryCode } from '../types'; const countryCodeMap: Record = { multi: { language: 'Multi', flag: '🌐', iso639: undefined }, + al: { language: 'Albanian', flag: '🇦🇱', iso639: 'sq' }, ar: { language: 'Arabic', flag: '🇸🇦', iso639: 'ara' }, bg: { language: 'Bulgarian', flag: '🇧🇬', iso639: 'bul' }, cs: { language: 'Czech', flag: '🇨🇿', iso639: 'ces' }, -- 2.45.2 From 9250d31bee8151a40e9ad5f251a62ebc97f4b937 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:46:45 +0200 Subject: [PATCH 4/9] Update index.ts --- src/source/index.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/source/index.ts b/src/source/index.ts index 466a308..aebb3fc 100644 --- a/src/source/index.ts +++ b/src/source/index.ts @@ -17,6 +17,7 @@ import { StreamKiste } from './StreamKiste'; import { VerHdLink } from './VerHdLink'; import { VidSrc } from './VidSrc'; import { VixSrc } from './VixSrc'; +import { Kokoshka } from './Kokoshka'; export * from './Source'; @@ -47,5 +48,8 @@ export const createSources = (fetcher: Fetcher): Source[] => { // IT new Eurostreaming(fetcher), new MostraGuarda(fetcher), + + // AL + new Kokoshka(fetcher), ].filter(source => !disabledSources.includes(source.id)); }; -- 2.45.2 From 0f3c66854e43ee395733c7a64e9eac0555106f6d Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:21:22 +0200 Subject: [PATCH 5/9] Update Kokoshka.ts Removed the part where the link of the episode was build based on imdb title format and added search and geting the correct link directly from the website html source. --- src/source/Kokoshka.ts | 95 ++++++++++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts index f40b28e..6eea294 100644 --- a/src/source/Kokoshka.ts +++ b/src/source/Kokoshka.ts @@ -49,50 +49,71 @@ export class Kokoshka extends Source { } }; - if (type === 'series') { - const cleanTitle = title - .toLowerCase() - .replace(/[:]/g, '') - .replace(/\s+/g, '-') - .replace(/[^\w-]/g, ''); + if (type === 'series') { + // Try to find the correct series link via search + const cleanTitle = title.replace(/[:]/g, ''); + const query = encodeURIComponent(`${cleanTitle} ${year}`); // include year in search + const searchUrl = new URL(`/?s=${query}`, this.baseUrl); + const searchHtml = await this.fetcher.text(ctx, searchUrl); - const episodeUrl = new URL( - `/episodi/${cleanTitle}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, - this.baseUrl - ); + // Find the first search result link + const linkMatch = searchHtml.match(/class=["']title["'][^>]*>]+data-post=['"](\d+)['"]/); - if (!postIdMatch) return []; - const postId = postIdMatch[1]; + const serieUrl = new URL(linkMatch[1], this.baseUrl); - // Try both tv servers - for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); - const json = await this.fetcher.json(ctx, ajaxUrl) as { embed_url?: string }; - if (!json?.embed_url) continue; + // Only continue if the URL contains "seriale" + if (!/seriale/i.test(serieUrl.pathname)) return []; - const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); - if (!embedId) continue; + // Extract the slug part for the series + const serieSlugMatch = serieUrl.pathname.match(/\/seriale\/([^/]+)/); + if (!serieSlugMatch?.[1]) return []; - const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); + let serieSlug = serieSlugMatch[1] + .replace(/-me-titra-shqip\/?$/, '') // remove suffix + .replace(/\/$/, ''); - if (await testUrl(ctx, finalUrl)) { - return [ - { - url: finalUrl, - meta: { - countryCodes: [CountryCode.al], - referer: this.baseUrl, - title: `${title} S${tmdbId.season}E${tmdbId.episode}`, - }, - }, - ]; - } - } + // Remove trailing year (e.g., -2025, -2019, etc.) + serieSlug = serieSlug.replace(/-\d{4}$/, ''); - return []; - } else { + // Build episode URL using the found series slug (without year) + const episodeUrl = new URL( + `/episodi/${serieSlug}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, + this.baseUrl + ); + + const episodeHtml = await this.fetcher.text(ctx, episodeUrl); + const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); + if (!postIdMatch) return []; + const postId = postIdMatch[1]; + + // Try both TV servers + for (const server of [1, 2]) { + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); + const json = (await this.fetcher.json(ctx, ajaxUrl)) as { embed_url?: string }; + if (!json?.embed_url) continue; + + const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); + if (!embedId) continue; + + const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); + + if (await testUrl(ctx, finalUrl)) { + return [ + { + url: finalUrl, + meta: { + countryCodes: [CountryCode.al], + referer: this.baseUrl, + title: `${title} S${tmdbId.season}E${tmdbId.episode}`, + }, + }, + ]; + } + } + + return []; +} else { // Movie const cleanTitle = title.replace(/[:]/g, ''); const query = encodeURIComponent(`${cleanTitle} ${year}`); -- 2.45.2 From bab7c80361bf5e46c57ba2fcf3875111dd42f4ee Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Wed, 15 Oct 2025 13:06:20 +0200 Subject: [PATCH 6/9] Update Kokoshka.ts Implemented automatic detection of servers...from my part i see only one result because dodo stream flags vercel ip and i am not able to get the md5 hash...so this needs to be tested by you with proxy or in the public instance... --- src/source/Kokoshka.ts | 177 +++++++++++++++++------------------------ 1 file changed, 75 insertions(+), 102 deletions(-) diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts index 6eea294..babe493 100644 --- a/src/source/Kokoshka.ts +++ b/src/source/Kokoshka.ts @@ -11,17 +11,16 @@ export class Kokoshka extends Source { 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) { + constructor(fetcher: Fetcher) { super(); this.fetcher = fetcher; } public async handleInternal(ctx: Context, type: string, imdbId: Id): Promise { try { - // 1. IMDb → TMDb + // --- 1. IMDb → TMDb --- let tmdbId: TmdbId; if (type === 'series') { const imdbBaseId = String(imdbId.id).split(':')[0]; @@ -35,126 +34,100 @@ export class Kokoshka extends Source { tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, { id: imdbId } as any); } - // 2. Title + year + // --- 2. Title + Year --- const [title, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId); + const cleanTitle = title.replace(/[:]/g, ''); + const query = encodeURIComponent(`${cleanTitle} ${year}`); + const searchUrl = new URL(`/?s=${query}`, this.baseUrl); + const searchHtml = await this.fetcher.text(ctx, searchUrl); - // Helper: check if a URL returns a valid page (not 404) - const testUrl = async (ctx: Context, url: URL): Promise => { + const linkMatch = searchHtml.match(/class=["']title["'][^>]*> { try { const html = await this.fetcher.text(ctx, url); - // crude but effective check for a dead embed page return !/not\s+found/i.test(html) && html.length > 100; - } catch { - return false; - } + } catch { return false; } }; - if (type === 'series') { - // Try to find the correct series link via search - const cleanTitle = title.replace(/[:]/g, ''); - const query = encodeURIComponent(`${cleanTitle} ${year}`); // include year in search - const searchUrl = new URL(`/?s=${query}`, this.baseUrl); - const searchHtml = await this.fetcher.text(ctx, searchUrl); + // --- SERIES --- + if (type === 'series') { + if (!/seriale/i.test(pageUrl.pathname)) return []; - // Find the first search result link - const linkMatch = searchHtml.match(/class=["']title["'][^>]*>]+data-post=['"](\d+)['"]/); - if (!postIdMatch) return []; - const postId = postIdMatch[1]; - - // Try both TV servers - for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); - const json = (await this.fetcher.json(ctx, ajaxUrl)) as { embed_url?: string }; - if (!json?.embed_url) continue; - - const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); - if (!embedId) continue; - - const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); - - if (await testUrl(ctx, finalUrl)) { - return [ - { - url: finalUrl, - meta: { - countryCodes: [CountryCode.al], - referer: this.baseUrl, - title: `${title} S${tmdbId.season}E${tmdbId.episode}`, - }, - }, - ]; - } - } - - return []; -} else { - // Movie - const cleanTitle = title.replace(/[:]/g, ''); - const query = encodeURIComponent(`${cleanTitle} ${year}`); - const searchUrl = new URL(`/?s=${query}`, this.baseUrl); - const searchHtml = await this.fetcher.text(ctx, searchUrl); - - const linkMatch = searchHtml.match(/class=["']title["'][^>]*>]+data-post=['"](\d+)['"]/); - if (!postIdMatch) return []; + const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); + if (!postIdMatch?.[1]) return []; const postId = postIdMatch[1]; - // Try both movie servers + const results: SourceResult[] = []; for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); - const json = await this.fetcher.json(ctx, ajaxUrl) as { embed_url?: string }; - if (!json?.embed_url) continue; + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); + const json = await this.fetcher.json(ctx, ajaxUrl) as any; - const embedId = json.embed_url.replace(/\\/g, '').split('/').pop(); - if (!embedId) continue; + const urls: string[] = []; + if (json.embed_url) urls.push(json.embed_url); + if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); - const finalUrl = new URL(`https://jilliandescribecompany.com/e/${embedId}`); - if (await testUrl(ctx, finalUrl)) { - return [ - { - url: finalUrl, + for (const embed of urls) { + const cleaned = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + const url = new URL(cleaned); + if (await testUrl(url)) { + results.push({ + url, meta: { countryCodes: [CountryCode.al], referer: this.baseUrl, - title, - }, - }, - ]; + title: `${title} S${tmdbId.season}E${tmdbId.episode}` + } + }); + } } } - - return []; + return results; } + + // --- MOVIE --- + const postIdMatch = pageHtml.match(/]+data-post=['"](\d+)['"]/); + if (!postIdMatch?.[1]) return []; + const postId = postIdMatch[1]; + + const results: SourceResult[] = []; + for (const server of [1, 2]) { + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); + const json = await this.fetcher.json(ctx, ajaxUrl) as any; + + const urls: string[] = []; + if (json.embed_url) urls.push(json.embed_url); + if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); + + for (const embed of urls) { + const cleaned = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + const url = new URL(cleaned); + if (await testUrl(url)) { + results.push({ + url, + meta: { + countryCodes: [CountryCode.al], + referer: this.baseUrl, + title, + } + }); + } + } + } + return results; + } catch (err) { console.error('Kokoshka handleInternal error:', err); return []; -- 2.45.2 From e126970047fc3ae039bbc2aa23934a8c3c1967a6 Mon Sep 17 00:00:00 2001 From: GSTAR <44748406+GLlgGL@users.noreply.github.com> Date: Wed, 15 Oct 2025 18:40:33 +0200 Subject: [PATCH 7/9] Update Kokoshka.ts There are some old dodo domains like d-s.io which fail to load the stream, so instead after fetching the link I replaced it with working domain like dsvplay.com --- src/source/Kokoshka.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts index babe493..f7ce030 100644 --- a/src/source/Kokoshka.ts +++ b/src/source/Kokoshka.ts @@ -79,9 +79,15 @@ export class Kokoshka extends Source { if (json.embed_url) urls.push(json.embed_url); if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); - for (const embed of urls) { - const cleaned = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); - const url = new URL(cleaned); + for (let embed of urls) { + embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + + // --- Replace Dodo domain --- + if (embed.startsWith('https://d-s.io/')) { + embed = embed.replace('https://d-s.io/e/', 'https://dsvplay.com/e/'); + } + + const url = new URL(embed); if (await testUrl(url)) { results.push({ url, @@ -111,9 +117,15 @@ export class Kokoshka extends Source { if (json.embed_url) urls.push(json.embed_url); if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); - for (const embed of urls) { - const cleaned = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); - const url = new URL(cleaned); + for (let embed of urls) { + embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + + // --- Replace Dodo domain --- + if (embed.startsWith('https://d-s.io/')) { + embed = embed.replace('https://d-s.io/e/', 'https://dsvplay.com/e/'); + } + + const url = new URL(embed); if (await testUrl(url)) { results.push({ url, @@ -133,4 +145,4 @@ export class Kokoshka extends Source { return []; } } -} +} \ No newline at end of file -- 2.45.2 From 484b59d54281c6c8d548dddf353ae1e1fcda1cb0 Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Thu, 16 Oct 2025 10:13:36 +0000 Subject: [PATCH 8/9] Small cleanups and update manifest snapshot --- src/source/Kokoshka.ts | 167 ++++++++---------- src/source/index.ts | 7 +- src/utils/__snapshots__/manifest.test.ts.snap | 9 +- src/utils/language.ts | 2 +- 4 files changed, 81 insertions(+), 104 deletions(-) diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts index f7ce030..e33a8dc 100644 --- a/src/source/Kokoshka.ts +++ b/src/source/Kokoshka.ts @@ -1,116 +1,67 @@ import { ContentType } from 'stremio-addon-sdk'; import { Context, CountryCode } from '../types'; -import { Fetcher, Id } from '../utils'; +import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils'; import { Source, SourceResult } from './Source'; -import { getTmdbIdFromImdbId, getTmdbNameAndYear } from '../utils/tmdb'; -import { TmdbId } from '../utils/id'; 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; - constructor(fetcher: Fetcher) { + public constructor(fetcher: Fetcher) { super(); + this.fetcher = fetcher; } - public async handleInternal(ctx: Context, type: string, imdbId: Id): Promise { - try { - // --- 1. IMDb → TMDb --- - let tmdbId: TmdbId; - if (type === 'series') { - const imdbBaseId = String(imdbId.id).split(':')[0]; - tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, { - id: imdbBaseId, - season: imdbId.season, - episode: imdbId.episode - } as any); - if (!tmdbId.season || !tmdbId.episode) return []; - } else { - tmdbId = await getTmdbIdFromImdbId(ctx, this.fetcher, { id: imdbId } as any); - } + public async handleInternal(ctx: Context, _type: string, id: Id): Promise { + const tmdbId = await getTmdbId(ctx, this.fetcher, id); - // --- 2. Title + Year --- - const [title, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId); - const cleanTitle = title.replace(/[:]/g, ''); - const query = encodeURIComponent(`${cleanTitle} ${year}`); - const searchUrl = new URL(`/?s=${query}`, this.baseUrl); - const searchHtml = await this.fetcher.text(ctx, searchUrl); + const [title, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId); + const cleanTitle = title.replace(/[:]/g, ''); + const query = encodeURIComponent(`${cleanTitle} ${year}`); + const searchUrl = new URL(`/?s=${query}`, this.baseUrl); + const searchHtml = await this.fetcher.text(ctx, searchUrl); - const linkMatch = searchHtml.match(/class=["']title["'][^>]*>]*> { - try { - const html = await this.fetcher.text(ctx, url); - return !/not\s+found/i.test(html) && html.length > 100; - } catch { return false; } - }; + // --- Helper: check if a URL returns valid page --- + const testUrl = async (url: URL) => { + try { + const html = await this.fetcher.text(ctx, url); + return !/not\s+found/i.test(html) && html.length > 100; + } catch { return false; } + }; - // --- SERIES --- - if (type === 'series') { - if (!/seriale/i.test(pageUrl.pathname)) return []; + if (tmdbId.season) { + if (!/seriale/i.test(pageUrl.pathname)) return []; - const serieSlugMatch = pageUrl.pathname.match(/\/seriale\/([^/]+)/); - if (!serieSlugMatch?.[1]) return []; - let serieSlug = serieSlugMatch[1].replace(/-me-titra-shqip\/?$/, '').replace(/\/$/, ''); - serieSlug = serieSlug.replace(/-\d{4}$/, ''); + const serieSlugMatch = pageUrl.pathname.match(/\/seriale\/([^/]+)/); + if (!serieSlugMatch?.[1]) return []; + let serieSlug = serieSlugMatch[1].replace(/-me-titra-shqip\/?$/, '').replace(/\/$/, ''); + serieSlug = serieSlug.replace(/-\d{4}$/, ''); - const episodeUrl = new URL(`/episodi/${serieSlug}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, this.baseUrl); - const episodeHtml = await this.fetcher.text(ctx, episodeUrl); + const episodeUrl = new URL(`/episodi/${serieSlug}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, this.baseUrl); + const episodeHtml = await this.fetcher.text(ctx, episodeUrl); - const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); - if (!postIdMatch?.[1]) return []; - const postId = postIdMatch[1]; - - const results: SourceResult[] = []; - for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); - const json = await this.fetcher.json(ctx, ajaxUrl) as any; - - const urls: string[] = []; - if (json.embed_url) urls.push(json.embed_url); - if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); - - for (let embed of urls) { - embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); - - // --- Replace Dodo domain --- - if (embed.startsWith('https://d-s.io/')) { - embed = embed.replace('https://d-s.io/e/', 'https://dsvplay.com/e/'); - } - - const url = new URL(embed); - if (await testUrl(url)) { - results.push({ - url, - meta: { - countryCodes: [CountryCode.al], - referer: this.baseUrl, - title: `${title} S${tmdbId.season}E${tmdbId.episode}` - } - }); - } - } - } - return results; - } - - // --- MOVIE --- - const postIdMatch = pageHtml.match(/]+data-post=['"](\d+)['"]/); + const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); if (!postIdMatch?.[1]) return []; const postId = postIdMatch[1]; const results: SourceResult[] = []; for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); const json = await this.fetcher.json(ctx, ajaxUrl) as any; const urls: string[] = []; @@ -120,11 +71,6 @@ export class Kokoshka extends Source { for (let embed of urls) { embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); - // --- Replace Dodo domain --- - if (embed.startsWith('https://d-s.io/')) { - embed = embed.replace('https://d-s.io/e/', 'https://dsvplay.com/e/'); - } - const url = new URL(embed); if (await testUrl(url)) { results.push({ @@ -132,17 +78,44 @@ export class Kokoshka extends Source { meta: { countryCodes: [CountryCode.al], referer: this.baseUrl, - title, - } + title: `${title} S${tmdbId.season}E${tmdbId.episode}`, + }, }); } } } return results; - - } catch (err) { - console.error('Kokoshka handleInternal error:', err); - return []; } + + const postIdMatch = pageHtml.match(/]+data-post=['"](\d+)['"]/); + if (!postIdMatch?.[1]) return []; + const postId = postIdMatch[1]; + + const results: SourceResult[] = []; + for (const server of [1, 2]) { + const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); + const json = await this.fetcher.json(ctx, ajaxUrl) as any; + + const urls: string[] = []; + if (json.embed_url) urls.push(json.embed_url); + if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); + + for (let embed of urls) { + embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + + const url = new URL(embed); + if (await testUrl(url)) { + results.push({ + url, + meta: { + countryCodes: [CountryCode.al], + referer: this.baseUrl, + title, + }, + }); + } + } + } + return results; } -} \ No newline at end of file +} diff --git a/src/source/index.ts b/src/source/index.ts index aebb3fc..b445314 100644 --- a/src/source/index.ts +++ b/src/source/index.ts @@ -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'; @@ -17,7 +18,6 @@ import { StreamKiste } from './StreamKiste'; import { VerHdLink } from './VerHdLink'; import { VidSrc } from './VidSrc'; import { VixSrc } from './VixSrc'; -import { Kokoshka } from './Kokoshka'; export * from './Source'; @@ -28,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 @@ -48,8 +50,5 @@ export const createSources = (fetcher: Fetcher): Source[] => { // IT new Eurostreaming(fetcher), new MostraGuarda(fetcher), - - // AL - new Kokoshka(fetcher), ].filter(source => !disabledSources.includes(source.id)); }; diff --git a/src/utils/__snapshots__/manifest.test.ts.snap b/src/utils/__snapshots__/manifest.test.ts.snap index 6877604..c54a4d8 100644 --- a/src/utils/__snapshots__/manifest.test.ts.snap +++ b/src/utils/__snapshots__/manifest.test.ts.snap @@ -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", diff --git a/src/utils/language.ts b/src/utils/language.ts index 93ef70c..d621d8a 100644 --- a/src/utils/language.ts +++ b/src/utils/language.ts @@ -2,7 +2,7 @@ import { CountryCode } from '../types'; const countryCodeMap: Record = { multi: { language: 'Multi', flag: '🌐', iso639: undefined }, - al: { language: 'Albanian', flag: '🇦🇱', iso639: 'sq' }, + al: { language: 'Albanian', flag: '🇦🇱', iso639: 'alb' }, ar: { language: 'Arabic', flag: '🇸🇦', iso639: 'ara' }, bg: { language: 'Bulgarian', flag: '🇧🇬', iso639: 'bul' }, cs: { language: 'Czech', flag: '🇨🇿', iso639: 'ces' }, -- 2.45.2 From 03af41d210b67bc8e41d8b948d8af84f611cd0ad Mon Sep 17 00:00:00 2001 From: WebStreamr <210764791+webstreamr@users.noreply.github.com> Date: Thu, 16 Oct 2025 14:27:29 +0000 Subject: [PATCH 9/9] adapt to follow usual structure and add tests --- src/source/Kokoshka.test.ts | 33 +++++ src/source/Kokoshka.ts | 136 ++++++++---------- .../https:api.themoviedb.org3movie1078605 | 1 + .../https:api.themoviedb.org3tv286801 | 1 + .../https:api.themoviedb.org3tv287877 | 1 + ...nster-the-ed-gein-story-1x3-me-titra-shqip | 12 ++ ...ka.digitalfilmaweapons-2025-me-titra-shqip | 12 ++ ...percent20dempercent20Friedhofpercent202025 | 7 + ...Edpercent20Geinpercent20Storypercent202025 | 7 + ...ttps:kokoshka.digitalsWeaponspercent202025 | 7 + ...ster-the-ed-gein-story-2025-me-titra-shqip | 11 ++ ...shka.digitalwp-jsondooplayerv2158117movie1 | 1 + ...shka.digitalwp-jsondooplayerv2158117movie2 | 1 + ...okoshka.digitalwp-jsondooplayerv2160439tv1 | 1 + ...okoshka.digitalwp-jsondooplayerv2160439tv2 | 1 + .../__snapshots__/Kokoshka.test.ts.snap | 55 +++++++ 16 files changed, 207 insertions(+), 80 deletions(-) create mode 100644 src/source/Kokoshka.test.ts create mode 100644 src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605 create mode 100644 src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801 create mode 100644 src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalepisodimonster-the-ed-gein-story-1x3-me-titra-shqip create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsDrunterpercent20percent26percent20DrpercentC3percentBCberpercent20-percent20Chaospercent20aufpercent20dempercent20Friedhofpercent202025 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsMonsterpercent20Thepercent20Edpercent20Geinpercent20Storypercent202025 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalserialemonster-the-ed-gein-story-2025-me-titra-shqip create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1 create mode 100644 src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2 create mode 100644 src/source/__snapshots__/Kokoshka.test.ts.snap diff --git a/src/source/Kokoshka.test.ts b/src/source/Kokoshka.test.ts new file mode 100644 index 0000000..0b046ff --- /dev/null +++ b/src/source/Kokoshka.test.ts @@ -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(); + }); +}); diff --git a/src/source/Kokoshka.ts b/src/source/Kokoshka.ts index e33a8dc..ecb8ca4 100644 --- a/src/source/Kokoshka.ts +++ b/src/source/Kokoshka.ts @@ -1,8 +1,14 @@ +import * as cheerio from 'cheerio'; import { ContentType } from 'stremio-addon-sdk'; import { Context, CountryCode } from '../types'; -import { Fetcher, getTmdbId, getTmdbNameAndYear, Id } from '../utils'; +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'; @@ -25,97 +31,67 @@ export class Kokoshka extends Source { public async handleInternal(ctx: Context, _type: string, id: Id): Promise { const tmdbId = await getTmdbId(ctx, this.fetcher, id); - const [title, year] = await getTmdbNameAndYear(ctx, this.fetcher, tmdbId); - const cleanTitle = title.replace(/[:]/g, ''); - const query = encodeURIComponent(`${cleanTitle} ${year}`); - const searchUrl = new URL(`/?s=${query}`, this.baseUrl); - const searchHtml = await this.fetcher.text(ctx, searchUrl); - - const linkMatch = searchHtml.match(/class=["']title["'][^>]*> { - try { - const html = await this.fetcher.text(ctx, url); - return !/not\s+found/i.test(html) && html.length > 100; - } catch { return false; } - }; + let pageUrl = await this.fetchPageUrl(ctx, tmdbId); + if (!pageUrl) { + return []; + } if (tmdbId.season) { - if (!/seriale/i.test(pageUrl.pathname)) return []; - - const serieSlugMatch = pageUrl.pathname.match(/\/seriale\/([^/]+)/); - if (!serieSlugMatch?.[1]) return []; - let serieSlug = serieSlugMatch[1].replace(/-me-titra-shqip\/?$/, '').replace(/\/$/, ''); - serieSlug = serieSlug.replace(/-\d{4}$/, ''); - - const episodeUrl = new URL(`/episodi/${serieSlug}-${tmdbId.season}x${tmdbId.episode}-me-titra-shqip/`, this.baseUrl); - const episodeHtml = await this.fetcher.text(ctx, episodeUrl); - - const postIdMatch = episodeHtml.match(/]+data-post=['"](\d+)['"]/); - if (!postIdMatch?.[1]) return []; - const postId = postIdMatch[1]; - - const results: SourceResult[] = []; - for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/tv/${server}`, this.baseUrl); - const json = await this.fetcher.json(ctx, ajaxUrl) as any; - - const urls: string[] = []; - if (json.embed_url) urls.push(json.embed_url); - if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); - - for (let embed of urls) { - embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); - - const url = new URL(embed); - if (await testUrl(url)) { - results.push({ - url, - meta: { - countryCodes: [CountryCode.al], - referer: this.baseUrl, - title: `${title} S${tmdbId.season}E${tmdbId.episode}`, - }, - }); - } - } + pageUrl = await this.fetchEpisodeUrl(ctx, pageUrl, tmdbId); + if (!pageUrl) { + return []; } - return results; } - const postIdMatch = pageHtml.match(/]+data-post=['"](\d+)['"]/); - if (!postIdMatch?.[1]) return []; - const postId = postIdMatch[1]; + const pageHtml = await this.fetcher.text(ctx, pageUrl); - const results: SourceResult[] = []; - for (const server of [1, 2]) { - const ajaxUrl = new URL(`/wp-json/dooplayer/v2/${postId}/movie/${server}`, this.baseUrl); - const json = await this.fetcher.json(ctx, ajaxUrl) as any; + const $ = cheerio.load(pageHtml); - const urls: string[] = []; - if (json.embed_url) urls.push(json.embed_url); - if (Array.isArray(json.sources)) urls.push(...json.sources.map((s: any) => s.file)); + const title = $('title').first().text().trim(); - for (let embed of urls) { - embed = embed.replace(/\\/g, '').replace(/^http:/, 'https:'); + 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 url = new URL(embed); - if (await testUrl(url)) { - results.push({ - url, + 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: this.baseUrl, + referer: pageUrl.href, title, }, - }); - } - } - } - return results; + }; + }) + .toArray(), + ); + } + + private readonly fetchPageUrl = async (ctx: Context, tmdbId: TmdbId): Promise => { + 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 { + 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); } } diff --git a/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605 b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605 new file mode 100644 index 0000000..6cd4cc4 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3movie1078605 @@ -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} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801 b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801 new file mode 100644 index 0000000..9b81dcb --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv286801 @@ -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} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877 b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877 new file mode 100644 index 0000000..59fb8a3 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:api.themoviedb.org3tv287877 @@ -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} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalepisodimonster-the-ed-gein-story-1x3-me-titra-shqip b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalepisodimonster-the-ed-gein-story-1x3-me-titra-shqip new file mode 100644 index 0000000..7142473 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalepisodimonster-the-ed-gein-story-1x3-me-titra-shqip @@ -0,0 +1,12 @@ +Monster: The Ed Gein Story: 1x3 me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip

Çfarë problemi ka?

KUJDES!!

Provoni të gjithë serverat para se të bëni një raportim! +Nese asnjë nga serverat nuk punon atëherë raportoni videon.

Ju lutemi vendosni adresen tuaj email për të marrë përgjigje
pasi të rregullohet problemi.

Zgjidh Serverin 519 Shikime Raporto Problem

  • Server 1chuckle-tube.com
  • Server 2dsvcom

Monster: The Ed Gein Story (2025) me Titra Shqip: 1x3

The Babysitter

Kërkimi i shtrembëruar i Edit për lidhje e çon atë nga një takim i vonë natën në një shfaqje groteske magjie – dhe ndez një mëri të hidhur që është e vështirë të hiqet dorë.

Monster: The Ed Gein Story: 1×3 me Titra Shqip
Oct. 03, 2025
Shpërndarë0
0 0 vota
Vlerësimi Filmit
Abonohu
Njoftoni për
guest

0 Komente
Më të rejat
Më e vjetrat Më të votuarit
Reagimet në linjë
Shiko të gjitha komentet
Kokoshka.Digital© 2025 - Filma me Titra Shqip
0
Do të donim mendin tuaj, ju lutemi komentoni.x

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip new file mode 100644 index 0000000..de703e7 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalfilmaweapons-2025-me-titra-shqip @@ -0,0 +1,12 @@ +Weapons (2025) me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip

Çfarë problemi ka?

KUJDES!!

Provoni të gjithë serverat para se të bëni një raportim! +Nese asnjë nga serverat nuk punon atëherë raportoni videon.

Ju lutemi vendosni adresen tuaj email për të marrë përgjigje
pasi të rregullohet problemi.

Zgjidh Serverin 4565 Shikime Raporto Problem

  • Shiko Trailerinyoutube.com
  • Server 1chuckle-tube.com
  • Server 2d-s.io
Weapons (2025) me Titra Shqip

Weapons (2025) me Titra Shqip

Mbrëmë në orën 2:17 të mëngjesit, të gjithë fëmijët e klasës së znj. Gandy u zgjuan, u ngritën nga shtrati, zbritën poshtë shkallëve, hapën derën e përparme, hynë në errësirë... dhe nuk u kthyen më kurrë.Aug. 04, 2025USA129 Min.R
Vlerësimi juaj: 0
7 3 vota

Përshkrimi i Filmit

Kur të gjithë fëmijët e së njëjtës klasë, përveç njërit, zhduken në mënyrë misterioze në të njëjtën natë dhe saktësisht në të njëjtën kohë, një komunitet mbetet duke vënë në pikëpyetje se kush ose çfarë fshihet pas zhdukjes së tyre.

Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Weapons (2025) me Titra Shqip
Titulli origjinal Weapons
Vlerësimi IMDb 7.8 102,043 vota
Vlerësimi TMDb 7.5 827 vota

Regjizori

Zach Cregger
Regjizori

Aktorët

Shpërndarë0

Tituj të Ngjashëm

The Exorcist: Believer (2023) me Titra Shqip
Cube (1997) me Titra Shqip
Things Will Be Different (2024) me Titra Shqip
Red Riding Hood (2011) me Titra Shqip
The Woman in the Window (2021) me Titra Shqip
The Retaliators (2022) me Titra Shqip
The Barber (2002) me Titra Shqip
The Hangman (2024) me Titra Shqip
Terrifier 3 (2024) me Titra Shqip
The Invitation (2022) me Titra Shqip
Apartment 18 (2014) me Titra Shqip
Countdown (2016) me Titra Shqip
0 0 vota
Vlerësimi Filmit
Abonohu
Njoftoni për
guest

0 Komente
Më të rejat
Më e vjetrat Më të votuarit
Reagimet në linjë
Shiko të gjitha komentet
Kokoshka.Digital© 2025 - Filma me Titra Shqip
0
Do të donim mendin tuaj, ju lutemi komentoni.x

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsDrunterpercent20percent26percent20DrpercentC3percentBCberpercent20-percent20Chaospercent20aufpercent20dempercent20Friedhofpercent202025 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsDrunterpercent20percent26percent20DrpercentC3percentBCberpercent20-percent20Chaospercent20aufpercent20dempercent20Friedhofpercent202025 new file mode 100644 index 0000000..228b03a --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsDrunterpercent20percent26percent20DrpercentC3percentBCberpercent20-percent20Chaospercent20aufpercent20dempercent20Friedhofpercent202025 @@ -0,0 +1,7 @@ +Drunter & Drüber - Chaos auf dem Friedhof 2025 - Kokoshka.Digital | Filma Me Titra Shqip
Kokoshka.Digital© 2025 - Filma me Titra Shqip

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsMonsterpercent20Thepercent20Edpercent20Geinpercent20Storypercent202025 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsMonsterpercent20Thepercent20Edpercent20Geinpercent20Storypercent202025 new file mode 100644 index 0000000..504a1bd --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsMonsterpercent20Thepercent20Edpercent20Geinpercent20Storypercent202025 @@ -0,0 +1,7 @@ +Monster The Ed Gein Story 2025 - Kokoshka.Digital | Filma Me Titra Shqip
Kokoshka.Digital© 2025 - Filma me Titra Shqip

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025 new file mode 100644 index 0000000..e8c9457 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalsWeaponspercent202025 @@ -0,0 +1,7 @@ +Weapons 2025 - Kokoshka.Digital | Filma Me Titra Shqip
Kokoshka.Digital© 2025 - Filma me Titra Shqip

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalserialemonster-the-ed-gein-story-2025-me-titra-shqip b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalserialemonster-the-ed-gein-story-2025-me-titra-shqip new file mode 100644 index 0000000..5ad81b9 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalserialemonster-the-ed-gein-story-2025-me-titra-shqip @@ -0,0 +1,11 @@ +Monster: The Ed Gein Story (2025) me Titra Shqip - Kokoshka.Digital | Filma Me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip

Monster: The Ed Gein Story (2025) me Titra Shqip

Oct. 03, 2025
Vlerësimi juaj: 0
10 2 vota

Sezone dhe episode

1Sezoni 1 Oct. 03, 2025
10

Regjizori

Ian Brennan
Regjizori

Aktorët

Laurie Metcalf isAugusta Gein
Augusta Gein
Tom Hollander isAlfred Hitchcock
Alfred Hitchcock
Olivia Williams isAlma Reville
Alma Reville
Suzanna Son isAdeline Watkins
Adeline Watkins
Vicky Krieps isIlse Koch
Ilse Koch
Lesley Manville isBernice Worden
Bernice Worden
Joey Pollari isAnthony Perkins
Anthony Perkins
Charlie Hall isFrank Worden
Frank Worden
Tyler Jacob Moore isSheriff Art Schley
Sheriff Art Schley

Video trailer

Përshkrimi i Filmit

Historia tronditëse e jetës së vërtetë e Ed Gein, vrasësit dhe grabitësit famëkeq të varreve që frymëzoi shumë nga vrasësit më ikonikë të Hollivudit.

Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Monster: The Ed Gein Story (2025) me Titra Shqip
Titulli origjinal Monster: The Ed Gein Story
Vlerësimi TMDb 7.542 84 vota
Transmetuar në dt: Oct. 03, 2025
Përfunduar në dt: Oct. 03, 2025
Sezone 1
Episodet 8
Shpërndarë0

Tituj të Ngjashëm

From Scratch (2022) me Titra Shqip
Prime Target (2025) me Titra Shqip
Unorthodox 2020
Solo (2016) me Titra Shqip
The Best Man: The Final Chapters (2022) me Titra Shqip
The Russell Murders: Who Killed Lin and Megan? (2023) me Titra Shqip
Deadly Class 2019
Ginny & Georgia (2021) me Titra Shqip
First Kill (2022) me Titra Shqip
Rome (2005) me Titra Shqip
Heartstopper (2022) me Titra Shqip
The Gifted (2017) me Titra Shqip
5 2 vota
Vlerësimi Filmit
Abonohu
Njoftoni për
guest

4 Komente
Më të rejat
Më e vjetrat Më të votuarit
Reagimet në linjë
Shiko të gjitha komentet

Kokoshka.Digital© 2025 - Filma me Titra Shqip
4
0
Do të donim mendin tuaj, ju lutemi komentoni.x

+ + \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1 new file mode 100644 index 0000000..759d0f0 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie1 @@ -0,0 +1 @@ +{"embed_url":"https:\/\/chuckle-tube.com\/e\/bfqvmxliwiaj","type":"iframe"} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2 new file mode 100644 index 0000000..751b359 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2158117movie2 @@ -0,0 +1 @@ +{"embed_url":"https:\/\/d-s.io\/e\/qjoay72ylazr","type":"iframe"} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1 new file mode 100644 index 0000000..f3367d5 --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv1 @@ -0,0 +1 @@ +{"embed_url":"https:\/\/chuckle-tube.com\/e\/iji4pltez4eq","type":"iframe"} \ No newline at end of file diff --git a/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2 b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2 new file mode 100644 index 0000000..570266e --- /dev/null +++ b/src/source/__fixtures__/Kokoshka/https:kokoshka.digitalwp-jsondooplayerv2160439tv2 @@ -0,0 +1 @@ +{"embed_url":"https:\/\/dsvplay.com\/e\/kle8q5fb816s","type":"iframe"} \ No newline at end of file diff --git a/src/source/__snapshots__/Kokoshka.test.ts.snap b/src/source/__snapshots__/Kokoshka.test.ts.snap new file mode 100644 index 0000000..7bff71e --- /dev/null +++ b/src/source/__snapshots__/Kokoshka.test.ts.snap @@ -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`] = `[]`; -- 2.45.2