Create Kokoshka.ts

This commit is contained in:
GSTAR 2025-10-14 21:36:55 +02:00 committed by WebStreamr
parent 9ab69d8461
commit 79b2beac70
No known key found for this signature in database

142
src/source/Kokoshka.ts Normal file
View file

@ -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<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);
}
// 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<boolean> => {
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(/<li[^>]+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["'][^>]*><a href=["']([^"']+)["']/);
if (!linkMatch?.[1]) return [];
const movieUrl = new URL(linkMatch[1], this.baseUrl);
const movieHtml = await this.fetcher.text(ctx, movieUrl);
const postIdMatch = movieHtml.match(/<li[^>]+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 [];
}
}
}