Small cleanups and update manifest snapshot

This commit is contained in:
WebStreamr 2025-10-16 10:13:36 +00:00
parent e126970047
commit 484b59d542
No known key found for this signature in database
4 changed files with 81 additions and 104 deletions

View file

@ -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<SourceResult[]> {
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<SourceResult[]> {
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["'][^>]*><a href=["']([^"']+)["']/);
if (!linkMatch?.[1]) return [];
const pageUrl = new URL(linkMatch[1], this.baseUrl);
const pageHtml = await this.fetcher.text(ctx, pageUrl);
const linkMatch = searchHtml.match(/class=["']title["'][^>]*><a href=["']([^"']+)["']/);
if (!linkMatch?.[1]) return [];
const pageUrl = new URL(linkMatch[1], this.baseUrl);
const pageHtml = await this.fetcher.text(ctx, pageUrl);
// --- 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; }
};
// --- 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(/<li[^>]+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(/<li[^>]+data-post=['"](\d+)['"]/);
const postIdMatch = episodeHtml.match(/<li[^>]+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(/<li[^>]+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;
}
}
}

View file

@ -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));
};

View file

@ -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",

View file

@ -2,7 +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: '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' },