easycats / addon.js
no1b4me's picture
Update addon.js
9216da2 verified
const { addonBuilder } = require('stremio-addon-sdk');
const EasynewsSearcher = require('./easynews-searcher');
const TMDBHandler = require('./tmdb-handler');
const winston = require('winston');
// Logger configuration
const logger = winston.createLogger({
level: 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.printf(({ level, message, timestamp }) => {
return `${timestamp} ${level}: ${message}`;
})
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
const manifest = {
id: 'org.stremio.easynews',
version: '1.0.0',
name: 'Easynews',
description: 'Stream movies and series from Easynews',
types: ['movie', 'series'],
catalogs: [
{
type: 'movie',
id: 'easynews-movie-catalog',
name: 'Easynews Movies',
extra: [{ name: 'search', isRequired: true }]
},
{
type: 'series',
id: 'easynews-series-catalog',
name: 'Easynews Series',
extra: [{ name: 'search', isRequired: true }]
}
],
resources: ['stream', 'catalog', 'meta'],
idPrefixes: ['tt', 'tmdb', 'easynews'],
background: 'https://www.easynews.com/images/logo.png',
logo: 'https://www.easynews.com/img/redesign/en-logo.svg',
contactEmail: '[email protected]'
};
// Cache for storing search results
const searchCache = new Map();
const CACHE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
let easynewsSearcher;
let tmdbHandler;
let easynewsUsername;
let easynewsPassword;
function generatePoster(content) {
const getBackgroundColor = (quality) => {
switch(quality) {
case '4K': return '#2c3e50';
case '1080p': return '#34495e';
case '720p': return '#2c3e50';
case '480p': return '#7f8c8d';
default: return '#95a5a6';
}
};
const bestQuality = Array.from(content.qualities).sort((a, b) => {
const order = { '4K': 4, '1080p': 3, '720p': 2, '480p': 1, 'SD': 0 };
return (order[b] || 0) - (order[a] || 0);
})[0];
const backgroundColor = getBackgroundColor(bestQuality);
// Create multiline text for title
let titleLines = [];
let words = content.title.split(' ');
let currentLine = '';
words.forEach(word => {
if ((currentLine + ' ' + word).length > 15) {
titleLines.push(currentLine);
currentLine = word;
} else {
currentLine = currentLine ? currentLine + ' ' + word : word;
}
});
if (currentLine) {
titleLines.push(currentLine);
}
if (titleLines.length > 3) {
titleLines = titleLines.slice(0, 3);
titleLines[2] += '...';
}
return `
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 450">
<rect width="100%" height="100%" fill="${backgroundColor}"/>
<circle cx="150" cy="100" r="50" fill="#e74c3c"/>
<text x="150" y="100" font-family="Arial" font-size="24" fill="white" text-anchor="middle" dominant-baseline="middle">${bestQuality}</text>
${titleLines.map((line, i) =>
`<text x="150" y="${220 + i * 30}" font-family="Arial" font-size="24" fill="white" text-anchor="middle">${line}</text>`
).join('')}
<text x="150" y="350" font-family="Arial" font-size="20" fill="white" text-anchor="middle">
${content.type === 'series'
? `S${content.season.toString().padStart(2, '0')}E${content.episode.toString().padStart(2, '0')}`
: content.year || ''}
</text>
<text x="150" y="400" font-family="Arial" font-size="16" fill="white" text-anchor="middle">
${content.streams.length} sources
</text>
<text x="150" y="430" font-family="Arial" font-size="14" fill="white" text-anchor="middle">
${Array.from(content.qualities).join(', ')}
</text>
</svg>
`.trim();
}
const builder = new addonBuilder(manifest);
builder.defineCatalogHandler(async ({ type, id, extra }) => {
if (!easynewsSearcher) {
logger.warn('Easynews searcher not initialized');
return { metas: [] };
}
const { search } = extra;
if (!search) {
logger.info('No search query provided');
return { metas: [] };
}
logger.info(`Searching Easynews catalog for: ${search}`);
try {
const results = await easynewsSearcher.search(search);
if (!results || results.length === 0) {
return { metas: [] };
}
// Group results by content
const groupedContent = new Map();
results.forEach(result => {
const isSeries = result.season !== null && result.episode !== null;
if ((type === 'movie' && isSeries) || (type === 'series' && !isSeries)) {
return;
}
let key;
if (isSeries) {
key = `${result.title} S${result.season.toString().padStart(2, '0')}E${result.episode.toString().padStart(2, '0')}`;
} else {
key = result.year ? `${result.title} (${result.year})` : result.title;
}
if (!groupedContent.has(key)) {
groupedContent.set(key, {
title: result.title,
year: result.year,
season: result.season,
episode: result.episode,
type: isSeries ? 'series' : 'movie',
streams: [],
qualities: new Set()
});
}
const content = groupedContent.get(key);
content.streams.push(result);
content.qualities.add(result.quality);
});
// Store in cache and create metas
const metas = Array.from(groupedContent.entries()).map(([key, content]) => {
const cacheKey = `easynews:${encodeURIComponent(key)}`;
searchCache.set(cacheKey, {
streams: content.streams,
timestamp: Date.now()
});
const bestQuality = Array.from(content.qualities).sort((a, b) => {
const order = { '4K': 4, '1080p': 3, '720p': 2, '480p': 1, 'SD': 0 };
return (order[b] || 0) - (order[a] || 0);
})[0];
// Enhanced meta object for better Android compatibility
return {
id: cacheKey,
type: content.type,
name: content.title,
year: content.year,
released: content.year ? `${content.year}-01-01` : undefined,
runtime: "120",
genres: content.type === 'movie' ? ['Movie'] : ['Series'],
background: null,
logo: null,
posterShape: 'regular',
poster: `data:image/svg+xml;base64,${Buffer.from(generatePoster(content)).toString('base64')}`,
description: `Quality: ${bestQuality}\nSources: ${content.streams.length}\nAvailable in: ${Array.from(content.qualities).join(', ')}`,
releaseInfo: content.year ? content.year.toString() : undefined,
imdbRating: null,
videos: content.type === 'series' ? [{
id: cacheKey,
title: `Season ${content.season} Episode ${content.episode}`,
season: content.season,
episode: content.episode,
released: content.year ? `${content.year}-01-01` : undefined
}] : undefined
};
});
// Sort by most recent year first
metas.sort((a, b) => (b.year || 0) - (a.year || 0));
return { metas };
} catch (error) {
logger.error(`Error in catalog handler: ${error.message}`);
return { metas: [] };
}
});
builder.defineStreamHandler(async ({ type, id }) => {
if (!easynewsSearcher) {
logger.warn('Easynews searcher not initialized');
return { streams: [] };
}
try {
let results = [];
if (id.startsWith('easynews:')) {
// Check cache first
const cached = searchCache.get(id);
if (cached && (Date.now() - cached.timestamp) < CACHE_TIMEOUT) {
results = cached.streams;
} else {
// If not in cache or expired, do a new search
const searchTerm = decodeURIComponent(id.replace('easynews:', ''));
results = await easynewsSearcher.search(searchTerm);
}
} else if ((id.startsWith('tt') || id.startsWith('tmdb')) && tmdbHandler) {
try {
const [baseId, seasonNum, episodeNum] = id.split(':');
const metadata = await tmdbHandler.getMetadata(baseId, type);
if (!metadata) {
logger.error(`Unable to find metadata for ${baseId}`);
return { streams: [] };
}
const title = metadata.title || metadata.name;
let year = null;
if (type === 'movie' && metadata.release_date) {
year = new Date(metadata.release_date).getFullYear();
} else if (type === 'series' && metadata.first_air_date) {
year = new Date(metadata.first_air_date).getFullYear();
}
let searchTerm;
if (type === 'series' && seasonNum && episodeNum) {
searchTerm = `${title} S${seasonNum.padStart(2, '0')}E${episodeNum.padStart(2, '0')}`;
} else if (type === 'movie') {
searchTerm = year ? `${title} ${year}` : title;
} else {
searchTerm = title;
}
results = await easynewsSearcher.search(searchTerm);
if (results.length === 0 && type === 'movie' && year) {
results = await easynewsSearcher.search(title);
}
} catch (error) {
logger.error(`TMDB error: ${error.message}. Falling back to ID-based search.`);
results = await handleIdBasedSearch(id, type);
}
} else {
results = await handleIdBasedSearch(id, type);
}
const streams = results.map(result => ({
title: result.filename,
name: `${result.quality} ${result.qualityEmoji} [${result.fileSize}]`,
url: result.linkUrl,
behaviorHints: {
notWebReady: true,
bingeGroup: `easynews-${result.quality}`,
proxyHeaders: {
request: {
"User-Agent": "VLC/3.0.0",
"Authorization": `Basic ${Buffer.from(`${easynewsUsername}:${easynewsPassword}`).toString('base64')}`
}
}
},
stream_quality: result.quality,
size: result.fileSize,
availability: 1
}));
// Sort streams by quality
streams.sort((a, b) => {
const qualityOrder = { '4K': 5, '1080p': 4, '720p': 3, '480p': 2, 'SD': 1 };
const qualityA = a.name.match(/(4K|1080p|720p|480p|SD)/i)?.[1]?.toUpperCase() || 'SD';
const qualityB = b.name.match(/(4K|1080p|720p|480p|SD)/i)?.[1]?.toUpperCase() || 'SD';
return (qualityOrder[qualityB] || 0) - (qualityOrder[qualityA] || 0);
});
return { streams };
} catch (error) {
logger.error(`Error in stream handler: ${error.message}`);
return { streams: [] };
}
});
// Helper function to handle ID-based searches when TMDB is not available
async function handleIdBasedSearch(id, type) {
const [baseId, seasonNum, episodeNum] = id.split(':');
let searchTerm;
if (type === 'series' && seasonNum && episodeNum) {
searchTerm = `${baseId} S${seasonNum.padStart(2, '0')}E${episodeNum.padStart(2, '0')}`;
} else {
searchTerm = baseId;
}
return await easynewsSearcher.search(searchTerm);
}
function setConfiguration(config) {
try {
logger.info('Received configuration:', JSON.stringify({
...config,
username: config.username ? '[REDACTED]' : undefined,
password: config.password ? '[REDACTED]' : undefined
}));
const { username, password } = config;
easynewsUsername = username;
easynewsPassword = password;
easynewsSearcher = new EasynewsSearcher(username, password);
const TMDB_API_KEY = process.env.TMDB_API_KEY || 'f051e7366c6105ad4f9aafe4733d9dae';
if (TMDB_API_KEY) {
try {
tmdbHandler = new TMDBHandler(TMDB_API_KEY);
logger.info('TMDB handler initialized with API key');
} catch (error) {
logger.warn('Failed to initialize TMDB handler:', error.message);
tmdbHandler = null;
}
} else {
logger.warn('No TMDB API key available - TMDB features will be disabled');
tmdbHandler = null;
}
logger.info('Configuration set for Easynews searcher');
return builder.getInterface();
} catch (error) {
logger.error('Error in setConfiguration:', error);
throw error;
}
}
builder.defineMetaHandler(async ({ type, id }) => {
logger.info(`Meta request for ${type}, id: ${id}`);
try {
// For cached Easynews results
if (id.startsWith('easynews:')) {
const cached = searchCache.get(id);
if (cached && (Date.now() - cached.timestamp) < CACHE_TIMEOUT) {
const result = cached.streams[0]; // Use first stream for metadata
return {
meta: {
id,
type: result.season ? 'series' : 'movie',
name: result.title,
year: result.year,
runtime: "120",
genres: result.season ? ['Series'] : ['Movie'],
posterShape: 'regular',
background: null,
logo: null,
description: `Available qualities: ${Array.from(new Set(cached.streams.map(s => s.quality))).join(', ')}\nTotal sources: ${cached.streams.length}`,
releaseInfo: result.year ? result.year.toString() : undefined,
imdbRating: null,
released: result.year ? `${result.year}-01-01` : undefined,
videos: result.season ? [{
id: id,
title: `Season ${result.season} Episode ${result.episode}`,
season: result.season,
episode: result.episode,
released: result.year ? `${result.year}-01-01` : undefined
}] : undefined
}
};
}
}
// For TMDB/IMDB IDs
if ((id.startsWith('tt') || id.startsWith('tmdb')) && tmdbHandler) {
const [baseId, seasonNum, episodeNum] = id.split(':');
const metadata = await tmdbHandler.getMetadata(baseId, type);
if (metadata) {
const meta = {
id: baseId,
type: type,
name: metadata.title || metadata.name,
year: metadata.release_date ? new Date(metadata.release_date).getFullYear() :
metadata.first_air_date ? new Date(metadata.first_air_date).getFullYear() : null,
runtime: metadata.runtime ? metadata.runtime.toString() : "120",
genres: metadata.genres ? metadata.genres.map(g => g.name) : [],
posterShape: 'regular',
background: metadata.backdrop_path ?
`https://image.tmdb.org/t/p/original${metadata.backdrop_path}` : null,
logo: null,
description: metadata.overview,
releaseInfo: metadata.release_date || metadata.first_air_date,
imdbRating: metadata.vote_average ? metadata.vote_average.toFixed(1) : null,
released: metadata.release_date || metadata.first_air_date,
};
if (type === 'series' && seasonNum && episodeNum) {
meta.videos = [{
id: id,
title: `Season ${seasonNum} Episode ${episodeNum}`,
season: parseInt(seasonNum),
episode: parseInt(episodeNum),
released: meta.released
}];
}
return { meta };
}
}
return { meta: null };
} catch (error) {
logger.error(`Error in meta handler: ${error.message}`);
return { meta: null };
}
});
module.exports = { setConfiguration };