Updated cacheing to use persistent json cache
This commit is contained in:
@@ -106,6 +106,11 @@ const API = {
|
||||
epg: {
|
||||
get: (sourceId) => API.request('GET', `/proxy/epg/${sourceId}`),
|
||||
getForChannels: (sourceId, channelIds) => API.request('POST', `/proxy/epg/${sourceId}/channels`, { channelIds })
|
||||
},
|
||||
|
||||
// Cache management
|
||||
cache: {
|
||||
clear: (sourceId) => API.request('DELETE', `/proxy/cache/${sourceId}`)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -294,10 +294,13 @@ class SourceManager {
|
||||
btn.textContent = '⏳';
|
||||
}
|
||||
|
||||
// Clear cache for this source first
|
||||
await API.proxy.cache.clear(id);
|
||||
|
||||
if (type === 'epg') {
|
||||
// Force refresh EPG data (server-side cache will be bypassed)
|
||||
// Force refresh EPG data
|
||||
if (window.app?.epgGuide) {
|
||||
await window.app.epgGuide.loadEpg(true); // Force refresh
|
||||
await window.app.epgGuide.loadEpg(true);
|
||||
}
|
||||
alert('EPG data refreshed!');
|
||||
} else if (type === 'xtream') {
|
||||
|
||||
@@ -12,8 +12,13 @@ class GuidePage {
|
||||
}
|
||||
|
||||
async show() {
|
||||
// Always reload EPG data when page is shown to ensure fresh data
|
||||
await this.app.epgGuide.loadEpg();
|
||||
// Only load EPG data if not already loaded
|
||||
if (!this.app.epgGuide.programmes || this.app.epgGuide.programmes.length === 0) {
|
||||
await this.app.epgGuide.loadEpg();
|
||||
} else {
|
||||
// Just re-render with existing data (updates time position)
|
||||
this.app.epgGuide.render();
|
||||
}
|
||||
}
|
||||
|
||||
hide() {
|
||||
|
||||
+77
-27
@@ -4,6 +4,10 @@ const { sources } = require('../db');
|
||||
const xtreamApi = require('../services/xtreamApi');
|
||||
const m3uParser = require('../services/m3uParser');
|
||||
const epgParser = require('../services/epgParser');
|
||||
const cache = require('../services/cache');
|
||||
|
||||
// Default cache TTL: 24 hours
|
||||
const DEFAULT_MAX_AGE_HOURS = 24;
|
||||
|
||||
/**
|
||||
* Proxy Xtream API calls
|
||||
@@ -11,15 +15,38 @@ const epgParser = require('../services/epgParser');
|
||||
*/
|
||||
router.get('/xtream/:sourceId/:action', async (req, res) => {
|
||||
try {
|
||||
const source = sources.getById(req.params.sourceId);
|
||||
const sourceId = req.params.sourceId;
|
||||
const source = sources.getById(sourceId);
|
||||
if (!source || source.type !== 'xtream') {
|
||||
return res.status(404).json({ error: 'Xtream source not found' });
|
||||
}
|
||||
|
||||
const api = xtreamApi.createFromSource(source);
|
||||
const { action } = req.params;
|
||||
const { category_id, stream_id, vod_id, series_id, limit } = req.query;
|
||||
const { category_id, stream_id, vod_id, series_id, limit, refresh, maxAge } = req.query;
|
||||
const forceRefresh = refresh === '1';
|
||||
const maxAgeHours = parseInt(maxAge) || DEFAULT_MAX_AGE_HOURS;
|
||||
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
||||
|
||||
// Actions that should be cached
|
||||
const cacheableActions = [
|
||||
'live_categories', 'live_streams',
|
||||
'vod_categories', 'vod_streams',
|
||||
'series_categories', 'series'
|
||||
];
|
||||
|
||||
// Build cache key (include category_id if present)
|
||||
const cacheKey = category_id ? `${action}_${category_id}` : action;
|
||||
|
||||
// Check cache for cacheable actions
|
||||
if (!forceRefresh && cacheableActions.includes(action)) {
|
||||
const cached = cache.get('xtream', sourceId, cacheKey, maxAgeMs);
|
||||
if (cached) {
|
||||
return res.json(cached);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch fresh data
|
||||
const api = xtreamApi.createFromSource(source);
|
||||
let data;
|
||||
switch (action) {
|
||||
case 'auth':
|
||||
@@ -56,6 +83,11 @@ router.get('/xtream/:sourceId/:action', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Unknown action' });
|
||||
}
|
||||
|
||||
// Cache the result for cacheable actions
|
||||
if (cacheableActions.includes(action)) {
|
||||
cache.set('xtream', sourceId, cacheKey, data);
|
||||
}
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error('Xtream proxy error:', err);
|
||||
@@ -92,12 +124,29 @@ router.get('/xtream/:sourceId/stream/:streamId/:type?', (req, res) => {
|
||||
*/
|
||||
router.get('/m3u/:sourceId', async (req, res) => {
|
||||
try {
|
||||
const source = sources.getById(req.params.sourceId);
|
||||
const sourceId = req.params.sourceId;
|
||||
const source = sources.getById(sourceId);
|
||||
if (!source || source.type !== 'm3u') {
|
||||
return res.status(404).json({ error: 'M3U source not found' });
|
||||
}
|
||||
|
||||
const forceRefresh = req.query.refresh === '1';
|
||||
const maxAgeHours = parseInt(req.query.maxAge) || DEFAULT_MAX_AGE_HOURS;
|
||||
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
||||
|
||||
// Check cache
|
||||
if (!forceRefresh) {
|
||||
const cached = cache.get('m3u', sourceId, 'playlist', maxAgeMs);
|
||||
if (cached) {
|
||||
return res.json(cached);
|
||||
}
|
||||
}
|
||||
|
||||
const data = await m3uParser.fetchAndParse(source.url);
|
||||
|
||||
// Store in cache
|
||||
cache.set('m3u', sourceId, 'playlist', data);
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
console.error('M3U proxy error:', err);
|
||||
@@ -106,13 +155,7 @@ router.get('/m3u/:sourceId', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* EPG cache - in-memory for performance
|
||||
* Structure: { [sourceId]: { data, fetchedAt } }
|
||||
*/
|
||||
const epgMemoryCache = {};
|
||||
|
||||
/**
|
||||
* Fetch and parse EPG (with caching)
|
||||
* Fetch and parse EPG (with file-based caching)
|
||||
* GET /api/proxy/epg/:sourceId
|
||||
* Query params:
|
||||
* - refresh=1 Force refresh, bypass cache
|
||||
@@ -127,15 +170,14 @@ router.get('/epg/:sourceId', async (req, res) => {
|
||||
}
|
||||
|
||||
const forceRefresh = req.query.refresh === '1';
|
||||
const maxAgeHours = parseInt(req.query.maxAge) || 24;
|
||||
const maxAgeHours = parseInt(req.query.maxAge) || DEFAULT_MAX_AGE_HOURS;
|
||||
const maxAgeMs = maxAgeHours * 60 * 60 * 1000;
|
||||
|
||||
// Check cache (unless force refresh)
|
||||
if (!forceRefresh && epgMemoryCache[sourceId]) {
|
||||
const cached = epgMemoryCache[sourceId];
|
||||
const age = Date.now() - cached.fetchedAt;
|
||||
if (age < maxAgeMs) {
|
||||
return res.json(cached.data);
|
||||
// Check file cache (unless force refresh)
|
||||
if (!forceRefresh) {
|
||||
const cached = cache.get('epg', sourceId, 'data', maxAgeMs);
|
||||
if (cached) {
|
||||
return res.json(cached);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,11 +190,8 @@ router.get('/epg/:sourceId', async (req, res) => {
|
||||
|
||||
const data = await epgParser.fetchAndParse(url);
|
||||
|
||||
// Store in cache
|
||||
epgMemoryCache[sourceId] = {
|
||||
data,
|
||||
fetchedAt: Date.now()
|
||||
};
|
||||
// Store in file cache
|
||||
cache.set('epg', sourceId, 'data', data);
|
||||
|
||||
res.json(data);
|
||||
} catch (err) {
|
||||
@@ -162,12 +201,22 @@ router.get('/epg/:sourceId', async (req, res) => {
|
||||
});
|
||||
|
||||
/**
|
||||
* Clear EPG cache for a source
|
||||
* Clear cache for a source
|
||||
* DELETE /api/proxy/cache/:sourceId
|
||||
*/
|
||||
router.delete('/cache/:sourceId', (req, res) => {
|
||||
const sourceId = req.params.sourceId;
|
||||
cache.clearSource(sourceId);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Clear EPG cache for a source (legacy endpoint, calls clearSource)
|
||||
* DELETE /api/proxy/epg/:sourceId/cache
|
||||
*/
|
||||
router.delete('/epg/:sourceId/cache', (req, res) => {
|
||||
const sourceId = req.params.sourceId;
|
||||
delete epgMemoryCache[sourceId];
|
||||
cache.clear('epg', sourceId, 'data');
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
@@ -266,9 +315,10 @@ router.get('/stream', async (req, res) => {
|
||||
return `${req.protocol}://${req.get('host')}${req.baseUrl}/stream?url=${encodeURIComponent(absoluteUrl)}`;
|
||||
} catch (e) { return line; }
|
||||
}).join('\n');
|
||||
|
||||
return res.send(manifest);
|
||||
}
|
||||
|
||||
// Return manifest (whether rewritten or not)
|
||||
return res.send(manifest);
|
||||
}
|
||||
|
||||
// Binary content (segments)
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* File-based Cache Service
|
||||
* Stores cached data as JSON files in data/cache/
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Cache directory
|
||||
const cacheDir = path.join(__dirname, '..', '..', 'data', 'cache');
|
||||
|
||||
// Ensure cache directories exist
|
||||
function ensureCacheDir(type, sourceId) {
|
||||
const dir = path.join(cacheDir, type, String(sourceId));
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
// Get cache file path
|
||||
function getCachePath(type, sourceId, key) {
|
||||
const dir = ensureCacheDir(type, sourceId);
|
||||
// Sanitize key for filename
|
||||
const safeKey = String(key || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
return path.join(dir, `${safeKey}.json`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached data if not expired
|
||||
* @param {string} type - Cache type (epg, m3u, xtream)
|
||||
* @param {number|string} sourceId - Source ID
|
||||
* @param {string} key - Cache key (e.g., action name)
|
||||
* @param {number} maxAgeMs - Maximum age in milliseconds
|
||||
* @returns {any|null} - Cached data or null if expired/missing
|
||||
*/
|
||||
function get(type, sourceId, key, maxAgeMs) {
|
||||
try {
|
||||
const cachePath = getCachePath(type, sourceId, key);
|
||||
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||
const age = Date.now() - cached.timestamp;
|
||||
|
||||
if (age > maxAgeMs) {
|
||||
return null; // Expired
|
||||
}
|
||||
|
||||
return cached.data;
|
||||
} catch (err) {
|
||||
console.warn(`Cache read error for ${type}/${sourceId}/${key}:`, err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store data in cache
|
||||
* @param {string} type - Cache type
|
||||
* @param {number|string} sourceId - Source ID
|
||||
* @param {string} key - Cache key
|
||||
* @param {any} data - Data to cache
|
||||
*/
|
||||
function set(type, sourceId, key, data) {
|
||||
try {
|
||||
const cachePath = getCachePath(type, sourceId, key);
|
||||
const cached = {
|
||||
timestamp: Date.now(),
|
||||
data: data
|
||||
};
|
||||
fs.writeFileSync(cachePath, JSON.stringify(cached));
|
||||
} catch (err) {
|
||||
console.error(`Cache write error for ${type}/${sourceId}/${key}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear specific cache entry
|
||||
*/
|
||||
function clear(type, sourceId, key) {
|
||||
try {
|
||||
const cachePath = getCachePath(type, sourceId, key);
|
||||
if (fs.existsSync(cachePath)) {
|
||||
fs.unlinkSync(cachePath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Cache clear error:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache for a source
|
||||
*/
|
||||
function clearSource(sourceId) {
|
||||
try {
|
||||
const types = ['epg', 'm3u', 'xtream'];
|
||||
for (const type of types) {
|
||||
const dir = path.join(cacheDir, type, String(sourceId));
|
||||
if (fs.existsSync(dir)) {
|
||||
fs.rmSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Cache clear source error:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
function clearAll() {
|
||||
try {
|
||||
if (fs.existsSync(cacheDir)) {
|
||||
fs.rmSync(cacheDir, { recursive: true });
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Cache clear all error:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache info for debugging
|
||||
*/
|
||||
function getInfo(type, sourceId, key) {
|
||||
try {
|
||||
const cachePath = getCachePath(type, sourceId, key);
|
||||
if (!fs.existsSync(cachePath)) {
|
||||
return null;
|
||||
}
|
||||
const cached = JSON.parse(fs.readFileSync(cachePath, 'utf-8'));
|
||||
const stats = fs.statSync(cachePath);
|
||||
return {
|
||||
timestamp: cached.timestamp,
|
||||
age: Date.now() - cached.timestamp,
|
||||
size: stats.size
|
||||
};
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get,
|
||||
set,
|
||||
clear,
|
||||
clearSource,
|
||||
clearAll,
|
||||
getInfo
|
||||
};
|
||||
Reference in New Issue
Block a user