mikmc/documentary
0
1require('dotenv').config();2const { addonBuilder, serveHTTP } = require('stremio-addon-sdk');3const axios = require('axios');4const fs = require('fs').promises;5const path = require('path');6const winston = require('winston');7const schedule = require('node-schedule');8 9const logger = winston.createLogger({10 level: 'debug',11 format: winston.format.combine(12 winston.format.timestamp(),13 winston.format.printf(({ level, message, timestamp }) => `${timestamp} ${level}: ${message}`)14 ),15 transports: [16 new winston.transports.Console(),17 new winston.transports.File({ filename: 'addon.log' })18 ]19});20 21const TMDB_API_KEY = process.env.TMDB_API_KEY;22const TMDB_BASE_URL = 'https://api.themoviedb.org/3';23const CACHE_DIR = path.join(__dirname, 'cache');24const CATALOG_CACHE_FILE = path.join(CACHE_DIR, 'catalog_cache.json');25const META_CACHE_FILE = path.join(CACHE_DIR, 'meta_cache.json');26 27const YEAR_START = process.env.YEAR_START || 2000;28const YEAR_END = process.env.YEAR_END || new Date().getFullYear();29const MIN_SCORE = parseFloat(process.env.MIN_SCORE) || 5;30const MAX_PAGES = parseInt(process.env.MAX_PAGES, 10) || 10;31const PAGE_SIZE = parseInt(process.env.PAGE_SIZE, 10) || 25;32 33const GENRES = `with_genres=99`; // Target documentaries34 35let catalogCache = {};36let metaCache = {};37 38const manifest = {39 id: 'org.stremio.custom-docs',40 version: '1.0.0',41 name: 'Hems Docs',42 description: 'Stremio Add-on showcasing the best documentaries',43 catalogs: [44 {45 id: 'custom-docs-catalog',46 name: 'Documentaries',47 type: 'movie',48 pageSize: PAGE_SIZE,49 extra: [{ name: 'search', isRequired: false }]50 }51 ],52 resources: ['catalog', 'meta'],53 types: ['movie'],54 idPrefixes: ['tmdb']55};56 57const builder = new addonBuilder(manifest);58 59async function ensureCacheDirectory() {60 try {61 await fs.mkdir(CACHE_DIR, { recursive: true });62 logger.debug('Cache directory ensured');63 } catch (error) {64 logger.error('Error creating cache directory:', error);65 }66}67 68async function loadCache() {69 try {70 const catalogData = await fs.readFile(CATALOG_CACHE_FILE, 'utf8');71 catalogCache = JSON.parse(catalogData);72 logger.debug(`Catalog cache loaded. Items: ${catalogCache.data ? catalogCache.data.length : 0}`);73 } catch (error) {74 logger.info('No existing catalog cache found. Will create a new one.');75 }76 77 try {78 const metaData = await fs.readFile(META_CACHE_FILE, 'utf8');79 metaCache = JSON.parse(metaData);80 logger.debug(`Meta cache loaded. Items: ${Object.keys(metaCache).length}`);81 } catch (error) {82 logger.info('No existing meta cache found. Will create a new one.');83 }84}85 86async function saveCache() {87 try {88 await fs.writeFile(CATALOG_CACHE_FILE, JSON.stringify(catalogCache));89 await fs.writeFile(META_CACHE_FILE, JSON.stringify(metaCache));90 logger.debug('Cache saved successfully');91 } catch (error) {92 logger.error('Error saving cache:', error);93 }94}95 96async function fetchMovies() {97 let page = 1;98 let totalPages = 1;99 const allResults = [];100 101 try {102 do {103 const url = `${TMDB_BASE_URL}/discover/movie?api_key=${TMDB_API_KEY}&include_adult=false&include_video=false&language=en&page=${page}&sort_by=primary_release_date.desc&primary_release_date.gte=${YEAR_START}-01-01&primary_release_date.lte=${YEAR_END}-12-31&vote_average.gte=${MIN_SCORE}&${GENRES}`;104 logger.debug(`Requesting TMDB: ${url}`);105 106 const response = await axios.get(url);107 const { results, total_pages } = response.data;108 totalPages = total_pages;109 110 logger.debug(`Page ${page}/${totalPages} fetched. Results: ${results.length}`);111 112 const validResults = results.filter(movie => movie.release_date && !isNaN(new Date(movie.release_date)));113 allResults.push(...validResults);114 115 page++;116 } while (page <= totalPages && page <= MAX_PAGES);117 118 allResults.sort((a, b) => new Date(b.release_date) - new Date(a.release_date));119 logger.info(`Total documentaries fetched: ${allResults.length}`);120 return allResults;121 } catch (error) {122 logger.error('Error fetching documentaries:', error);123 return [];124 }125}126 127async function fetchMovieMetadata(movieId) {128 try {129 const url = `${TMDB_BASE_URL}/movie/${movieId}?api_key=${TMDB_API_KEY}`;130 const response = await axios.get(url);131 return response.data;132 } catch (error) {133 logger.error(`Error fetching metadata for movie ID ${movieId}:`, error.message);134 return { error: 'Failed to fetch movie data' };135 }136}137 138async function updateCatalogAndMetaCache() {139 logger.info('Updating catalog and meta cache...');140 try {141 const results = await fetchMovies();142 catalogCache = {143 timestamp: Date.now(),144 data: results145 };146 147 for (const movie of results) {148 const movieId = movie.id;149 logger.debug(`Fetching meta for movie ID: ${movieId}`);150 const movieData = await fetchMovieMetadata(movieId);151 metaCache[movieId] = {152 timestamp: Date.now(),153 data: movieData154 };155 }156 157 await saveCache();158 logger.info(`Catalog and meta cache updated. Items: ${results.length}`);159 } catch (error) {160 logger.error('Error updating catalog and meta cache:', error);161 }162}163 164builder.defineCatalogHandler(async (args) => {165 const searchQuery = args.extra && args.extra.search ? args.extra.search : '';166 logger.debug(`Catalog request received. Query: "${searchQuery}"`);167 168 let results = catalogCache.data || [];169 logger.debug(`Catalog cache has ${results.length} items`);170 171 if (searchQuery) {172 results = results.filter(movie =>173 movie.title.toLowerCase().includes(searchQuery.toLowerCase())174 );175 logger.debug(`Filtered results for "${searchQuery}": ${results.length}`);176 }177 178 const metas = results.map(item => ({179 id: `tmdb-${item.id}`,180 type: 'movie',181 name: item.title,182 poster: item.poster_path ? `https://image.tmdb.org/t/p/w500${item.poster_path}` : null,183 description: item.overview || 'No description available',184 releaseInfo: item.release_date ? item.release_date.split('-')[0] : 'Unknown'185 }));186 187 logger.info(`Catalog request processed. Query: "${searchQuery}", Results: ${metas.length}`);188 return { metas };189});190 191builder.defineMetaHandler(async (args) => {192 const id = args.id.split('-')[1];193 logger.debug(`Meta request received for movie ID: ${id}`);194 195 if (!metaCache[id]) {196 logger.warn(`Meta not found for movie ID: ${id}`);197 return { meta: null };198 }199 200 const movieData = metaCache[id].data;201 202 if (movieData.error) {203 logger.warn(`Meta request failed for movie ID: ${id}`);204 return { meta: null };205 }206 207 const { 208 title, 209 overview, 210 poster_path, 211 release_date, 212 genres, 213 runtime, 214 vote_average, 215 backdrop_path 216 } = movieData;217 218 logger.info(`Meta request processed for movie ID: ${id}`);219 return {220 meta: {221 id: args.id,222 type: 'movie',223 name: title || 'Unknown Title',224 poster: poster_path ? `https://image.tmdb.org/t/p/w500${poster_path}` : null,225 background: backdrop_path ? `https://image.tmdb.org/t/p/w1280${backdrop_path}` : null,226 description: overview || 'No description available',227 releaseInfo: release_date ? release_date.split('-')[0] : 'Unknown',228 genre: genres && Array.isArray(genres) ? genres.map(g => g.name) : [],229 runtime: runtime ? `${runtime} min` : 'Unknown',230 rating: vote_average ? vote_average.toFixed(1) : 'N/A',231 }232 };233});234 235async function startServer() {236 await ensureCacheDirectory();237 await loadCache();238 239 async function refreshCache() {240 logger.info('Scheduled update: Refreshing catalog and meta cache');241 await updateCatalogAndMetaCache();242 }243 244 schedule.scheduleJob('0 0 * * *', refreshCache);245 246 if (!catalogCache.timestamp || Date.now() - catalogCache.timestamp > 24 * 60 * 60 * 1000) {247 logger.info('Initial cache refresh needed');248 await refreshCache();249 } else {250 logger.info('Cache is up to date. Next refresh scheduled for midnight');251 }252 253 const interface = builder.getInterface();254 const port = process.env.PORT || 8055;255 256 logger.info(`Starting Stremio Add-on server on port ${port}`);257 serveHTTP(interface, { port });258}259 260startServer();261 