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] 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' },