feat: enhance plugin system with async support and lifecycle hooks & reverted PR #79 due to Node.js v24 stream compatibility issues
This commit is contained in:
+869
@@ -0,0 +1,869 @@
|
|||||||
|
diff --git a/public/index.html b/public/index.html
|
||||||
|
index dd74211..9205ef7 100644
|
||||||
|
--- a/public/index.html
|
||||||
|
+++ b/public/index.html
|
||||||
|
@@ -9,7 +9,7 @@
|
||||||
|
<meta name="mobile-web-app-capable" content="yes">
|
||||||
|
<title>nodecast-tv | IPTV Player</title>
|
||||||
|
<meta name="description" content="Stream live TV, movies, and series with EPG support">
|
||||||
|
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
|
<link rel="stylesheet" href="css/main.css">
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
@@ -1095,20 +1095,20 @@
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- JavaScript -->
|
||||||
|
- <script src="/js/icons.js"></script>
|
||||||
|
- <script src="/js/api.js?v=2"></script>
|
||||||
|
- <script src="/js/components/VideoPlayer.js?v=2"></script>
|
||||||
|
- <script src="/js/components/ChannelList.js?v=3"></script>
|
||||||
|
- <script src="/js/components/SourceManager.js?v=2"></script>
|
||||||
|
- <script src="/js/components/EpgGuide.js?v=2"></script>
|
||||||
|
- <script src="/js/pages/HomePage.js?v=2"></script>
|
||||||
|
- <script src="/js/pages/LivePage.js?v=2"></script>
|
||||||
|
- <script src="/js/pages/Guide.js?v=2"></script>
|
||||||
|
- <script src="/js/pages/MoviesPage.js?v=4"></script>
|
||||||
|
- <script src="/js/pages/SeriesPage.js?v=1"></script>
|
||||||
|
- <script src="/js/pages/Settings.js?v=2"></script>
|
||||||
|
- <script src="/js/pages/WatchPage.js?v=1"></script>
|
||||||
|
- <script src="/js/app.js?v=4"></script>
|
||||||
|
+ <script src="js/icons.js"></script>
|
||||||
|
+ <script src="js/api.js?v=2"></script>
|
||||||
|
+ <script src="js/components/VideoPlayer.js?v=2"></script>
|
||||||
|
+ <script src="js/components/ChannelList.js?v=3"></script>
|
||||||
|
+ <script src="js/components/SourceManager.js?v=2"></script>
|
||||||
|
+ <script src="js/components/EpgGuide.js?v=2"></script>
|
||||||
|
+ <script src="js/pages/HomePage.js?v=2"></script>
|
||||||
|
+ <script src="js/pages/LivePage.js?v=2"></script>
|
||||||
|
+ <script src="js/pages/Guide.js?v=2"></script>
|
||||||
|
+ <script src="js/pages/MoviesPage.js?v=4"></script>
|
||||||
|
+ <script src="js/pages/SeriesPage.js?v=1"></script>
|
||||||
|
+ <script src="js/pages/Settings.js?v=2"></script>
|
||||||
|
+ <script src="js/pages/WatchPage.js?v=1"></script>
|
||||||
|
+ <script src="js/app.js?v=4"></script>
|
||||||
|
<script>
|
||||||
|
// Check for SSO token in URL
|
||||||
|
(function () {
|
||||||
|
@@ -1125,4 +1125,4 @@
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
-</html>
|
||||||
|
\ No newline at end of file
|
||||||
|
+</html>
|
||||||
|
diff --git a/public/js/api.js b/public/js/api.js
|
||||||
|
index 77b1306..ed4b354 100644
|
||||||
|
--- a/public/js/api.js
|
||||||
|
+++ b/public/js/api.js
|
||||||
|
@@ -24,7 +24,7 @@ const API = {
|
||||||
|
options.body = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
- const response = await fetch(`/api${endpoint}`, options);
|
||||||
|
+ const response = await fetch(`api/${endpoint}`, options);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
@@ -39,7 +39,7 @@ const API = {
|
||||||
|
// If unauthorized, redirect to login
|
||||||
|
if (response.status === 401) {
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
- window.location.href = '/login.html';
|
||||||
|
+ window.location.href = 'login.html';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(result.error || `Server responded with ${response.status}`);
|
||||||
|
@@ -50,37 +50,37 @@ const API = {
|
||||||
|
|
||||||
|
// Sources
|
||||||
|
sources: {
|
||||||
|
- getAll: () => API.request('GET', '/sources'),
|
||||||
|
- getByType: (type) => API.request('GET', `/sources/type/${type}`),
|
||||||
|
- getById: (id) => API.request('GET', `/sources/${id}`),
|
||||||
|
- create: (data) => API.request('POST', '/sources', data),
|
||||||
|
- update: (id, data) => API.request('PUT', `/sources/${id}`, data),
|
||||||
|
- delete: (id) => API.request('DELETE', `/sources/${id}`),
|
||||||
|
- toggle: (id) => API.request('POST', `/sources/${id}/toggle`),
|
||||||
|
- test: (id) => API.request('POST', `/sources/${id}/test`),
|
||||||
|
- sync: (id) => API.request('POST', `/sources/${id}/sync`), // Manual sync
|
||||||
|
- getStatus: () => API.request('GET', '/sources/status'), // Get all statuses
|
||||||
|
- estimate: (id) => API.request('GET', `/sources/${id}/estimate`), // Estimate M3U size
|
||||||
|
- estimateByUrl: (url, type) => API.request('POST', '/sources/estimate', { url, type }), // Estimate by URL (before creation)
|
||||||
|
+ getAll: () => API.request('GET', 'sources'),
|
||||||
|
+ getByType: (type) => API.request('GET', `sources/type/${type}`),
|
||||||
|
+ getById: (id) => API.request('GET', `sources/${id}`),
|
||||||
|
+ create: (data) => API.request('POST', 'sources', data),
|
||||||
|
+ update: (id, data) => API.request('PUT', `sources/${id}`, data),
|
||||||
|
+ delete: (id) => API.request('DELETE', `sources/${id}`),
|
||||||
|
+ toggle: (id) => API.request('POST', `sources/${id}/toggle`),
|
||||||
|
+ test: (id) => API.request('POST', `sources/${id}/test`),
|
||||||
|
+ sync: (id) => API.request('POST', `sources/${id}/sync`), // Manual sync
|
||||||
|
+ getStatus: () => API.request('GET', 'sources/status'), // Get all statuses
|
||||||
|
+ estimate: (id) => API.request('GET', `sources/${id}/estimate`), // Estimate M3U size
|
||||||
|
+ estimateByUrl: (url, type) => API.request('POST', 'sources/estimate', { url, type }), // Estimate by URL (before creation)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Channels (hidden items)
|
||||||
|
channels: {
|
||||||
|
- getHidden: (sourceId = null) => API.request('GET', `/channels/hidden${sourceId ? `?sourceId=${sourceId}` : ''}`),
|
||||||
|
- hide: (sourceId, itemType, itemId) => API.request('POST', '/channels/hide', { sourceId, itemType, itemId }),
|
||||||
|
- show: (sourceId, itemType, itemId) => API.request('POST', '/channels/show', { sourceId, itemType, itemId }),
|
||||||
|
- isHidden: (sourceId, itemType, itemId) => API.request('GET', `/channels/hidden/check?sourceId=${sourceId}&itemType=${itemType}&itemId=${itemId}`),
|
||||||
|
- bulkHide: (items) => API.request('POST', '/channels/hide/bulk', { items }),
|
||||||
|
- bulkShow: (items) => API.request('POST', '/channels/show/bulk', { items }),
|
||||||
|
+ getHidden: (sourceId = null) => API.request('GET', `channels/hidden${sourceId ? `?sourceId=${sourceId}` : ''}`),
|
||||||
|
+ hide: (sourceId, itemType, itemId) => API.request('POST', 'channels/hide', { sourceId, itemType, itemId }),
|
||||||
|
+ show: (sourceId, itemType, itemId) => API.request('POST', 'channels/show', { sourceId, itemType, itemId }),
|
||||||
|
+ isHidden: (sourceId, itemType, itemId) => API.request('GET', `channels/hidden/check?sourceId=${sourceId}&itemType=${itemType}&itemId=${itemId}`),
|
||||||
|
+ bulkHide: (items) => API.request('POST', 'channels/hide/bulk', { items }),
|
||||||
|
+ bulkShow: (items) => API.request('POST', 'channels/show/bulk', { items }),
|
||||||
|
// Fast bulk operations - single SQL statement
|
||||||
|
- showAll: (sourceId, contentType) => API.request('POST', '/channels/show/all', { sourceId, contentType }),
|
||||||
|
- hideAll: (sourceId, contentType) => API.request('POST', '/channels/hide/all', { sourceId, contentType })
|
||||||
|
+ showAll: (sourceId, contentType) => API.request('POST', 'channels/show/all', { sourceId, contentType }),
|
||||||
|
+ hideAll: (sourceId, contentType) => API.request('POST', 'channels/hide/all', { sourceId, contentType })
|
||||||
|
},
|
||||||
|
|
||||||
|
// Favorites
|
||||||
|
favorites: {
|
||||||
|
getAll: (sourceId = null, itemType = null) => {
|
||||||
|
- let url = '/favorites';
|
||||||
|
+ let url = 'favorites';
|
||||||
|
const params = [];
|
||||||
|
if (sourceId) params.push(`sourceId=${sourceId}`);
|
||||||
|
if (itemType) params.push(`itemType=${itemType}`);
|
||||||
|
@@ -88,84 +88,84 @@ const API = {
|
||||||
|
return API.request('GET', url);
|
||||||
|
},
|
||||||
|
add: (sourceId, itemId, itemType = 'channel') =>
|
||||||
|
- API.request('POST', '/favorites', { sourceId, itemId, itemType }),
|
||||||
|
+ API.request('POST', 'favorites', { sourceId, itemId, itemType }),
|
||||||
|
remove: (sourceId, itemId, itemType = 'channel') =>
|
||||||
|
- API.request('DELETE', '/favorites', { sourceId, itemId, itemType }),
|
||||||
|
+ API.request('DELETE', 'favorites', { sourceId, itemId, itemType }),
|
||||||
|
check: (sourceId, itemId, itemType = 'channel') =>
|
||||||
|
- API.request('GET', `/favorites/check?sourceId=${sourceId}&itemId=${itemId}&itemType=${itemType}`)
|
||||||
|
+ API.request('GET', `favorites/check?sourceId=${sourceId}&itemId=${itemId}&itemType=${itemType}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Proxy
|
||||||
|
proxy: {
|
||||||
|
// Xtream
|
||||||
|
xtream: {
|
||||||
|
- auth: (sourceId) => API.request('GET', `/proxy/xtream/${sourceId}/auth`),
|
||||||
|
+ auth: (sourceId) => API.request('GET', `proxy/xtream/${sourceId}/auth`),
|
||||||
|
liveCategories: (sourceId, options = {}) => {
|
||||||
|
const params = options.includeHidden ? '?includeHidden=true' : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/live_categories${params}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/live_categories${params}`);
|
||||||
|
},
|
||||||
|
liveStreams: (sourceId, categoryId = null, options = {}) => {
|
||||||
|
const params = [];
|
||||||
|
if (categoryId) params.push(`category_id=${categoryId}`);
|
||||||
|
if (options.includeHidden) params.push('includeHidden=true');
|
||||||
|
const query = params.length ? `?${params.join('&')}` : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/live_streams${query}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/live_streams${query}`);
|
||||||
|
},
|
||||||
|
vodCategories: (sourceId, options = {}) => {
|
||||||
|
const params = options.includeHidden ? '?includeHidden=true' : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/vod_categories${params}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/vod_categories${params}`);
|
||||||
|
},
|
||||||
|
vodStreams: (sourceId, categoryId = null, options = {}) => {
|
||||||
|
const params = [];
|
||||||
|
if (categoryId) params.push(`category_id=${categoryId}`);
|
||||||
|
if (options.includeHidden) params.push('includeHidden=true');
|
||||||
|
const query = params.length ? `?${params.join('&')}` : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/vod_streams${query}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/vod_streams${query}`);
|
||||||
|
},
|
||||||
|
seriesCategories: (sourceId, options = {}) => {
|
||||||
|
const params = options.includeHidden ? '?includeHidden=true' : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/series_categories${params}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/series_categories${params}`);
|
||||||
|
},
|
||||||
|
series: (sourceId, categoryId = null, options = {}) => {
|
||||||
|
const params = [];
|
||||||
|
if (categoryId) params.push(`category_id=${categoryId}`);
|
||||||
|
if (options.includeHidden) params.push('includeHidden=true');
|
||||||
|
const query = params.length ? `?${params.join('&')}` : '';
|
||||||
|
- return API.request('GET', `/proxy/xtream/${sourceId}/series${query}`);
|
||||||
|
+ return API.request('GET', `proxy/xtream/${sourceId}/series${query}`);
|
||||||
|
},
|
||||||
|
seriesInfo: (sourceId, seriesId) =>
|
||||||
|
- API.request('GET', `/proxy/xtream/${sourceId}/series_info?series_id=${seriesId}`),
|
||||||
|
- shortEpg: (sourceId, streamId) => API.request('GET', `/proxy/xtream/${sourceId}/short_epg?stream_id=${streamId}`),
|
||||||
|
+ API.request('GET', `proxy/xtream/${sourceId}/series_info?series_id=${seriesId}`),
|
||||||
|
+ shortEpg: (sourceId, streamId) => API.request('GET', `proxy/xtream/${sourceId}/short_epg?stream_id=${streamId}`),
|
||||||
|
getStreamUrl: (sourceId, streamId, type = 'live', container = 'm3u8') =>
|
||||||
|
- API.request('GET', `/proxy/xtream/${sourceId}/stream/${streamId}/${type}?container=${container}`)
|
||||||
|
+ API.request('GET', `proxy/xtream/${sourceId}/stream/${streamId}/${type}?container=${container}`)
|
||||||
|
},
|
||||||
|
|
||||||
|
// EPG
|
||||||
|
epg: {
|
||||||
|
- get: (sourceId) => API.request('GET', `/proxy/epg/${sourceId}`),
|
||||||
|
- getForChannels: (sourceId, channelIds) => API.request('POST', `/proxy/epg/${sourceId}/channels`, { channelIds })
|
||||||
|
+ 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}`)
|
||||||
|
+ clear: (sourceId) => API.request('DELETE', `proxy/cache/${sourceId}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
settings: {
|
||||||
|
- get: () => API.request('GET', '/settings'),
|
||||||
|
- update: (data) => API.request('PUT', '/settings', data),
|
||||||
|
- reset: () => API.request('DELETE', '/settings'),
|
||||||
|
- getDefaults: () => API.request('GET', '/settings/defaults')
|
||||||
|
+ get: () => API.request('GET', 'settings'),
|
||||||
|
+ update: (data) => API.request('PUT', 'settings', data),
|
||||||
|
+ reset: () => API.request('DELETE', 'settings'),
|
||||||
|
+ getDefaults: () => API.request('GET', 'settings/defaults')
|
||||||
|
},
|
||||||
|
|
||||||
|
// Users (admin only)
|
||||||
|
users: {
|
||||||
|
- getAll: () => API.request('GET', '/auth/users'),
|
||||||
|
- create: (data) => API.request('POST', '/auth/users', data),
|
||||||
|
- update: (id, data) => API.request('PUT', `/auth/users/${id}`, data),
|
||||||
|
- delete: (id) => API.request('DELETE', `/auth/users/${id}`)
|
||||||
|
+ getAll: () => API.request('GET', 'auth/users'),
|
||||||
|
+ create: (data) => API.request('POST', 'auth/users', data),
|
||||||
|
+ update: (id, data) => API.request('PUT', `auth/users/${id}`, data),
|
||||||
|
+ delete: (id) => API.request('DELETE', `auth/users/${id}`)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
diff --git a/public/js/app.js b/public/js/app.js
|
||||||
|
index c4d2f8c..f00dc38 100644
|
||||||
|
--- a/public/js/app.js
|
||||||
|
+++ b/public/js/app.js
|
||||||
|
@@ -169,13 +169,13 @@ class App {
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
// No token, redirect to login (replace to avoid back button issues)
|
||||||
|
- window.location.replace('/login.html');
|
||||||
|
+ window.location.replace('login.html');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify token with server
|
||||||
|
- const response = await fetch('/api/auth/me', {
|
||||||
|
+ const response = await fetch('api/auth/me', {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
}
|
||||||
|
@@ -201,7 +201,7 @@ class App {
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Authentication error:', err);
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
- window.location.replace('/login.html');
|
||||||
|
+ window.location.replace('login.html');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -225,7 +225,7 @@ class App {
|
||||||
|
|
||||||
|
const token = localStorage.getItem('authToken');
|
||||||
|
if (token) {
|
||||||
|
- await fetch('/api/auth/logout', {
|
||||||
|
+ await fetch('api/auth/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
@@ -234,7 +234,7 @@ class App {
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('authToken');
|
||||||
|
- window.location.replace('/login.html');
|
||||||
|
+ window.location.replace('login.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
navbar.appendChild(logoutLink);
|
||||||
|
@@ -283,7 +283,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.app = new App();
|
||||||
|
|
||||||
|
// Fetch and display version badge
|
||||||
|
- fetch('/api/version')
|
||||||
|
+ fetch('api/version')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
const badge = document.getElementById('version-badge');
|
||||||
|
diff --git a/public/js/components/ChannelList.js b/public/js/components/ChannelList.js
|
||||||
|
index e8da5ad..2047dbf 100644
|
||||||
|
--- a/public/js/components/ChannelList.js
|
||||||
|
+++ b/public/js/components/ChannelList.js
|
||||||
|
@@ -33,10 +33,10 @@ class ChannelList {
|
||||||
|
* Only proxies HTTP URLs when on HTTPS page
|
||||||
|
*/
|
||||||
|
getProxiedImageUrl(url) {
|
||||||
|
- if (!url || url.length === 0) return '/img/placeholder.png';
|
||||||
|
+ if (!url || url.length === 0) return 'img/placeholder.png';
|
||||||
|
// Only proxy if we're on HTTPS and the image is HTTP
|
||||||
|
if (window.location.protocol === 'https:' && url.startsWith('http://')) {
|
||||||
|
- return `/api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
@@ -510,7 +510,7 @@ class ChannelList {
|
||||||
|
data-render-id="${renderId}"
|
||||||
|
data-render-group="${renderGroup}">
|
||||||
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
|
- alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
|
+ alt="" onerror="this.onerror=null;this.src='img/placeholder.png'">
|
||||||
|
<div class="channel-info">
|
||||||
|
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||||
|
<div class="channel-program">${this.escapeHtml(this.getProgramInfo(channel) || '')}</div>
|
||||||
|
@@ -623,7 +623,7 @@ class ChannelList {
|
||||||
|
data-render-id="${renderId}"
|
||||||
|
data-render-group="${renderGroup}">
|
||||||
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
|
- alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
|
+ alt="" onerror="this.onerror=null;this.src='img/placeholder.png'">
|
||||||
|
<div class="channel-info">
|
||||||
|
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||||
|
<div class="channel-program">${this.escapeHtml(this.getProgramInfo(channel) || '')}</div>
|
||||||
|
@@ -1052,7 +1052,7 @@ class ChannelList {
|
||||||
|
|
||||||
|
div.innerHTML = `
|
||||||
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
|
- alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
|
+ alt="" onerror="this.onerror=null;this.src='img/placeholder.png'">
|
||||||
|
<div class="channel-info">
|
||||||
|
<div class="channel-name">${this.escapeHtml(channel.name)}</div>
|
||||||
|
<div class="channel-program">${this.getProgramInfo(channel) || ''}</div>
|
||||||
|
@@ -1305,7 +1305,7 @@ class ChannelList {
|
||||||
|
<div class="epg-info-modal">
|
||||||
|
<div class="channel-details">
|
||||||
|
<img class="channel-logo" src="${this.getProxiedImageUrl(channel.tvgLogo)}"
|
||||||
|
- onerror="this.onerror=null;this.src='/img/placeholder.png'" />
|
||||||
|
+ onerror="this.onerror=null;this.src='img/placeholder.png'" />
|
||||||
|
<div class="channel-meta">
|
||||||
|
<p><strong>Group:</strong> ${this.escapeHtml(channel.groupTitle || 'Uncategorized')}</p>
|
||||||
|
<p><strong>Source:</strong> ${channel.sourceType}</p>
|
||||||
|
diff --git a/public/js/components/EpgGuide.js b/public/js/components/EpgGuide.js
|
||||||
|
index ce4a01d..7b0da96 100644
|
||||||
|
--- a/public/js/components/EpgGuide.js
|
||||||
|
+++ b/public/js/components/EpgGuide.js
|
||||||
|
@@ -43,10 +43,10 @@ class EpgGuide {
|
||||||
|
* Only proxies HTTP URLs when on HTTPS page
|
||||||
|
*/
|
||||||
|
getProxiedImageUrl(url) {
|
||||||
|
- if (!url || url.length === 0) return '/img/placeholder.png';
|
||||||
|
+ if (!url || url.length === 0) return 'img/placeholder.png';
|
||||||
|
// Only proxy if we're on HTTPS and the image is HTTP
|
||||||
|
if (window.location.protocol === 'https:' && url.startsWith('http://')) {
|
||||||
|
- return `/api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/proxy/image?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
@@ -212,7 +212,7 @@ class EpgGuide {
|
||||||
|
// Load EPG from ALL sources in parallel
|
||||||
|
const fetchPromises = sources.map(async (source) => {
|
||||||
|
try {
|
||||||
|
- const response = await fetch(`/api/proxy/epg/${source.id}${queryParams}`);
|
||||||
|
+ const response = await fetch(`api/proxy/epg/${source.id}${queryParams}`);
|
||||||
|
if (!response.ok) throw new Error(`Status ${response.status}`);
|
||||||
|
return await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
@@ -596,7 +596,7 @@ class EpgGuide {
|
||||||
|
${isFavorite ? Icons.favorite : Icons.favoriteOutline}
|
||||||
|
</button>
|
||||||
|
<img class="epg-channel-logo" src="${logo}"
|
||||||
|
- alt="" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
|
+ alt="" onerror="this.onerror=null;this.src='img/placeholder.png'">
|
||||||
|
<span class="epg-channel-name">${name}</span>
|
||||||
|
<div class="resize-handle"></div>
|
||||||
|
</div>
|
||||||
|
diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js
|
||||||
|
index a089dab..ac3b71c 100644
|
||||||
|
--- a/public/js/components/VideoPlayer.js
|
||||||
|
+++ b/public/js/components/VideoPlayer.js
|
||||||
|
@@ -805,7 +805,7 @@ class VideoPlayer {
|
||||||
|
async startTranscodeSession(url, options = {}) {
|
||||||
|
try {
|
||||||
|
console.log('[Player] Starting HLS transcode session...', options);
|
||||||
|
- const res = await fetch('/api/transcode/session', {
|
||||||
|
+ const res = await fetch('api/transcode/session', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url, ...options })
|
||||||
|
@@ -817,7 +817,7 @@ class VideoPlayer {
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Player] Session start failed:', err);
|
||||||
|
// Fallback to direct transcode if session fails
|
||||||
|
- return `/api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -829,7 +829,7 @@ class VideoPlayer {
|
||||||
|
console.log('[Player] Stopping transcode session:', this.currentSessionId);
|
||||||
|
try {
|
||||||
|
// Fire and forget cleanup
|
||||||
|
- fetch(`/api/transcode/${this.currentSessionId}`, { method: 'DELETE' });
|
||||||
|
+ fetch(`api/transcode/${this.currentSessionId}`, { method: 'DELETE' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to stop session:', err);
|
||||||
|
}
|
||||||
|
@@ -865,7 +865,7 @@ class VideoPlayer {
|
||||||
|
if (this.settings.autoTranscode) {
|
||||||
|
console.log('[Player] Auto Transcode enabled. Probing stream...');
|
||||||
|
try {
|
||||||
|
- const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
|
||||||
|
+ const probeRes = await fetch(`api/probe?url=${encodeURIComponent(streamUrl)}`);
|
||||||
|
const info = await probeRes.json();
|
||||||
|
console.log(`[Player] Probe result: video=${info.video}, audio=${info.audio}, ${info.width}x${info.height}, compatible=${info.compatible}`);
|
||||||
|
|
||||||
|
@@ -885,7 +885,7 @@ class VideoPlayer {
|
||||||
|
track.kind = 'subtitles';
|
||||||
|
track.label = sub.title;
|
||||||
|
track.srclang = sub.language;
|
||||||
|
- track.src = `/api/subtitle?url=${encodeURIComponent(streamUrl)}&index=${sub.index}`;
|
||||||
|
+ track.src = `api/subtitle?url=${encodeURIComponent(streamUrl)}&index=${sub.index}`;
|
||||||
|
this.video.appendChild(track);
|
||||||
|
});
|
||||||
|
|
||||||
|
@@ -925,7 +925,7 @@ class VideoPlayer {
|
||||||
|
// Raw .ts container - use remux
|
||||||
|
console.log('[Player] Auto: Using remux (.ts container)');
|
||||||
|
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
|
||||||
|
- const remuxUrl = `/api/remux?url=${encodeURIComponent(streamUrl)}`;
|
||||||
|
+ const remuxUrl = `api/remux?url=${encodeURIComponent(streamUrl)}`;
|
||||||
|
this.currentUrl = remuxUrl;
|
||||||
|
this.video.src = remuxUrl;
|
||||||
|
this.video.play().catch(e => {
|
||||||
|
@@ -997,7 +997,7 @@ class VideoPlayer {
|
||||||
|
// Probe to get video codec for HEVC tag handling
|
||||||
|
let videoCodec = 'unknown';
|
||||||
|
try {
|
||||||
|
- const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(streamUrl)}`);
|
||||||
|
+ const probeRes = await fetch(`api/probe?url=${encodeURIComponent(streamUrl)}`);
|
||||||
|
const info = await probeRes.json();
|
||||||
|
videoCodec = info.video;
|
||||||
|
} catch (e) { console.warn('Probe failed for force audio, assuming h264'); }
|
||||||
|
@@ -1096,7 +1096,7 @@ class VideoPlayer {
|
||||||
|
(data.type === Hls.ErrorTypes.MEDIA_ERROR && data.details === 'fragParsingError');
|
||||||
|
|
||||||
|
// Don't proxy if it's already a local API URL
|
||||||
|
- const isLocalApi = this.currentUrl.startsWith('/api/');
|
||||||
|
+ const isLocalApi = this.currentUrl.startsWith('api/');
|
||||||
|
|
||||||
|
if (isCorsLikely && !this.isUsingProxy && !isLocalApi) {
|
||||||
|
console.log('CORS/Network error detected, retrying via proxy...', data.details);
|
||||||
|
@@ -1344,14 +1344,14 @@ class VideoPlayer {
|
||||||
|
* Get proxied URL for a stream
|
||||||
|
*/
|
||||||
|
getProxiedUrl(url) {
|
||||||
|
- return `/api/proxy/stream?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/proxy/stream?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get transcoded URL for a stream (audio transcoding for browser compatibility)
|
||||||
|
*/
|
||||||
|
getTranscodeUrl(url) {
|
||||||
|
- return `/api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@@ -1359,7 +1359,7 @@ class VideoPlayer {
|
||||||
|
* Used for raw .ts streams that browsers can't play directly
|
||||||
|
*/
|
||||||
|
getRemuxUrl(url) {
|
||||||
|
- return `/api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
diff --git a/public/js/login.js b/public/js/login.js
|
||||||
|
index a246d10..e2339f3 100644
|
||||||
|
--- a/public/js/login.js
|
||||||
|
+++ b/public/js/login.js
|
||||||
|
@@ -5,10 +5,10 @@ document.getElementById('login-form').addEventListener('submit', async (e) => {
|
||||||
|
const password = document.getElementById('password').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
- const response = await API.request('POST', '/auth/login', { username, password });
|
||||||
|
+ const response = await API.request('POST', 'auth/login', { username, password });
|
||||||
|
localStorage.setItem('sessionToken', response.token);
|
||||||
|
- window.location.href = '/';
|
||||||
|
+ window.location.href = 'index.html';
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('login-error').textContent = 'Login failed: ' + err.message;
|
||||||
|
}
|
||||||
|
-});
|
||||||
|
\ No newline at end of file
|
||||||
|
+});
|
||||||
|
diff --git a/public/js/pages/HomePage.js b/public/js/pages/HomePage.js
|
||||||
|
index 6ddadc1..c8a1805 100644
|
||||||
|
--- a/public/js/pages/HomePage.js
|
||||||
|
+++ b/public/js/pages/HomePage.js
|
||||||
|
@@ -174,7 +174,7 @@ class HomePage {
|
||||||
|
await this.renderFavoriteChannels();
|
||||||
|
|
||||||
|
// 1. Load Watch History
|
||||||
|
- const history = await window.API.request('GET', '/history?limit=12');
|
||||||
|
+ const history = await window.API.request('GET', 'history?limit=12');
|
||||||
|
if (history && Array.isArray(history)) {
|
||||||
|
this.renderHistory(history);
|
||||||
|
}
|
||||||
|
@@ -197,7 +197,7 @@ class HomePage {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch favorite channels for current user
|
||||||
|
- const favorites = await window.API.request('GET', '/favorites?itemType=channel');
|
||||||
|
+ const favorites = await window.API.request('GET', 'favorites?itemType=channel');
|
||||||
|
|
||||||
|
if (!favorites || favorites.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state hint">Add channels to favorites from Live TV</div>';
|
||||||
|
@@ -251,14 +251,14 @@ class HomePage {
|
||||||
|
}
|
||||||
|
|
||||||
|
createChannelTile(channel) {
|
||||||
|
- const logo = channel.tvgLogo || '/img/placeholder.png';
|
||||||
|
- const logoUrl = logo.startsWith('http') ? `/api/proxy/image?url=${encodeURIComponent(logo)}` : logo;
|
||||||
|
+ const logo = channel.tvgLogo || 'img/placeholder.png';
|
||||||
|
+ const logoUrl = logo.startsWith('http') ? `api/proxy/image?url=${encodeURIComponent(logo)}` : logo;
|
||||||
|
const name = channel.name || 'Unknown';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="channel-tile" data-channel-id="${channel.id}" data-source-id="${channel.sourceId}">
|
||||||
|
<div class="tile-logo">
|
||||||
|
- <img src="${logoUrl}" alt="${name}" loading="lazy" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||||
|
+ <img src="${logoUrl}" alt="${name}" loading="lazy" onerror="this.onerror=null;this.src='img/placeholder.png'">
|
||||||
|
</div>
|
||||||
|
<div class="tile-name" title="${name}">${name}</div>
|
||||||
|
</div>
|
||||||
|
@@ -352,7 +352,7 @@ class HomePage {
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
- const movies = await window.API.request('GET', '/channels/recent?type=movie&limit=12');
|
||||||
|
+ const movies = await window.API.request('GET', 'channels/recent?type=movie&limit=12');
|
||||||
|
if (!movies || movies.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state hint">No recently added movies found</div>';
|
||||||
|
return;
|
||||||
|
@@ -381,7 +381,7 @@ class HomePage {
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
- const series = await window.API.request('GET', '/channels/recent?type=series&limit=12');
|
||||||
|
+ const series = await window.API.request('GET', 'channels/recent?type=series&limit=12');
|
||||||
|
if (!series || series.length === 0) {
|
||||||
|
list.innerHTML = '<div class="empty-state hint">No recently added series found</div>';
|
||||||
|
return;
|
||||||
|
@@ -411,13 +411,13 @@ class HomePage {
|
||||||
|
const percent = Math.min(100, Math.round((progress / duration) * 100));
|
||||||
|
|
||||||
|
// Proxy the poster if it's an external URL
|
||||||
|
- const poster = data.poster || '/img/poster-placeholder.jpg';
|
||||||
|
- const posterUrl = poster.startsWith('http') ? `/api/proxy/image?url=${encodeURIComponent(poster)}` : poster;
|
||||||
|
+ const poster = data.poster || 'img/poster-placeholder.jpg';
|
||||||
|
+ const posterUrl = poster.startsWith('http') ? `api/proxy/image?url=${encodeURIComponent(poster)}` : poster;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="dashboard-card" data-id="${item_id}" data-type="${type}">
|
||||||
|
<div class="card-image">
|
||||||
|
- <img src="${posterUrl}" alt="${data.title || item.name}" loading="lazy" onerror="this.onerror=null;this.src='/img/poster-placeholder.jpg'">
|
||||||
|
+ <img src="${posterUrl}" alt="${data.title || item.name}" loading="lazy" onerror="this.onerror=null;this.src='img/poster-placeholder.jpg'">
|
||||||
|
<div class="progress-bar-container">
|
||||||
|
<div class="progress-bar" style="width: ${percent}%"></div>
|
||||||
|
</div>
|
||||||
|
@@ -436,13 +436,13 @@ class HomePage {
|
||||||
|
createRecentCard(item) {
|
||||||
|
const { data, item_id } = item;
|
||||||
|
const type = item.type || item.item_type;
|
||||||
|
- const poster = item.stream_icon || data.poster || '/img/poster-placeholder.jpg';
|
||||||
|
- const posterUrl = poster.startsWith('http') ? `/api/proxy/image?url=${encodeURIComponent(poster)}` : poster;
|
||||||
|
+ const poster = item.stream_icon || data.poster || 'img/poster-placeholder.jpg';
|
||||||
|
+ const posterUrl = poster.startsWith('http') ? `api/proxy/image?url=${encodeURIComponent(poster)}` : poster;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="dashboard-card" data-id="${item_id}" data-type="${type}">
|
||||||
|
<div class="card-image">
|
||||||
|
- <img src="${posterUrl}" alt="${item.name}" loading="lazy" onerror="this.onerror=null;this.src='/img/poster-placeholder.jpg'">
|
||||||
|
+ <img src="${posterUrl}" alt="${item.name}" loading="lazy" onerror="this.onerror=null;this.src='img/poster-placeholder.jpg'">
|
||||||
|
<div class="play-icon-overlay">
|
||||||
|
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||||
|
</div>
|
||||||
|
@@ -465,7 +465,7 @@ class HomePage {
|
||||||
|
const streamId = item.item_id;
|
||||||
|
const container = item.container_extension || (item.data && item.data.containerExtension) || 'mp4';
|
||||||
|
|
||||||
|
- const result = await window.API.request('GET', `/proxy/xtream/${sourceId}/stream/${streamId}/${streamType}?container=${container}`);
|
||||||
|
+ const result = await window.API.request('GET', `proxy/xtream/${sourceId}/stream/${streamId}/${streamType}?container=${container}`);
|
||||||
|
|
||||||
|
if (result && result.url) {
|
||||||
|
const content = {
|
||||||
|
@@ -488,7 +488,7 @@ class HomePage {
|
||||||
|
// Fetch seriesInfo if we have a seriesId
|
||||||
|
if (content.seriesId && sourceId) {
|
||||||
|
try {
|
||||||
|
- const seriesInfo = await window.API.request('GET', `/proxy/xtream/${sourceId}/series_info?series_id=${content.seriesId}`);
|
||||||
|
+ const seriesInfo = await window.API.request('GET', `proxy/xtream/${sourceId}/series_info?series_id=${content.seriesId}`);
|
||||||
|
if (seriesInfo) {
|
||||||
|
content.seriesInfo = seriesInfo;
|
||||||
|
}
|
||||||
|
diff --git a/public/js/pages/MoviesPage.js b/public/js/pages/MoviesPage.js
|
||||||
|
index a5bb40f..c27fba6 100644
|
||||||
|
--- a/public/js/pages/MoviesPage.js
|
||||||
|
+++ b/public/js/pages/MoviesPage.js
|
||||||
|
@@ -278,7 +278,7 @@ class MoviesPage {
|
||||||
|
card.dataset.movieId = movie.stream_id;
|
||||||
|
card.dataset.sourceId = movie.sourceId;
|
||||||
|
|
||||||
|
- const poster = movie.stream_icon || movie.cover || '/img/placeholder.png';
|
||||||
|
+ const poster = movie.stream_icon || movie.cover || 'img/placeholder.png';
|
||||||
|
const year = movie.year || movie.releaseDate?.substring(0, 4) || '';
|
||||||
|
const rating = movie.rating ? `${Icons.star} ${movie.rating}` : '';
|
||||||
|
|
||||||
|
@@ -287,7 +287,7 @@ class MoviesPage {
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="movie-poster">
|
||||||
|
<img src="${poster}" alt="${movie.name}"
|
||||||
|
- onerror="this.onerror=null;this.src='/img/placeholder.png'" loading="lazy">
|
||||||
|
+ onerror="this.onerror=null;this.src='img/placeholder.png'" loading="lazy">
|
||||||
|
<div class="movie-play-overlay">
|
||||||
|
<span class="play-icon">${Icons.play}</span>
|
||||||
|
</div>
|
||||||
|
diff --git a/public/js/pages/SeriesPage.js b/public/js/pages/SeriesPage.js
|
||||||
|
index 6b7a929..1a25f85 100644
|
||||||
|
--- a/public/js/pages/SeriesPage.js
|
||||||
|
+++ b/public/js/pages/SeriesPage.js
|
||||||
|
@@ -288,7 +288,7 @@ class SeriesPage {
|
||||||
|
card.dataset.seriesId = series.series_id;
|
||||||
|
card.dataset.sourceId = series.sourceId;
|
||||||
|
|
||||||
|
- const poster = series.cover || '/img/placeholder.png';
|
||||||
|
+ const poster = series.cover || 'img/placeholder.png';
|
||||||
|
const year = series.year || series.releaseDate?.substring(0, 4) || '';
|
||||||
|
const rating = series.rating ? `${Icons.star} ${series.rating}` : '';
|
||||||
|
|
||||||
|
@@ -297,7 +297,7 @@ class SeriesPage {
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="series-poster">
|
||||||
|
<img src="${poster}" alt="${series.name}"
|
||||||
|
- onerror="this.onerror=null;this.src='/img/placeholder.png'" loading="lazy">
|
||||||
|
+ onerror="this.onerror=null;this.src='img/placeholder.png'" loading="lazy">
|
||||||
|
<div class="series-play-overlay">
|
||||||
|
<span class="play-icon">${Icons.play}</span>
|
||||||
|
</div>
|
||||||
|
@@ -350,7 +350,7 @@ class SeriesPage {
|
||||||
|
this.detailsPanel.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Set header info
|
||||||
|
- document.getElementById('series-poster').src = series.cover || '/img/placeholder.png';
|
||||||
|
+ document.getElementById('series-poster').src = series.cover || 'img/placeholder.png';
|
||||||
|
document.getElementById('series-title').textContent = series.name;
|
||||||
|
document.getElementById('series-plot').textContent = series.plot || '';
|
||||||
|
|
||||||
|
diff --git a/public/js/pages/Settings.js b/public/js/pages/Settings.js
|
||||||
|
index a47b5d7..437d7a5 100644
|
||||||
|
--- a/public/js/pages/Settings.js
|
||||||
|
+++ b/public/js/pages/Settings.js
|
||||||
|
@@ -259,7 +259,7 @@ class SettingsPage {
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
- const response = await fetch('/api/settings/hw-info');
|
||||||
|
+ const response = await fetch('api/settings/hw-info');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch hardware info');
|
||||||
|
const hwInfo = await response.json();
|
||||||
|
|
||||||
|
@@ -603,7 +603,7 @@ class SettingsPage {
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fetch last sync time from server
|
||||||
|
- const response = await fetch('/api/settings/sync-status');
|
||||||
|
+ const response = await fetch('api/settings/sync-status');
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch sync status');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
diff --git a/public/js/pages/WatchPage.js b/public/js/pages/WatchPage.js
|
||||||
|
index b85ca63..2aecdfd 100644
|
||||||
|
--- a/public/js/pages/WatchPage.js
|
||||||
|
+++ b/public/js/pages/WatchPage.js
|
||||||
|
@@ -329,7 +329,7 @@ class WatchPage {
|
||||||
|
async startTranscodeSession(url, options = {}) {
|
||||||
|
try {
|
||||||
|
console.log('[WatchPage] Starting HLS transcode session...', options);
|
||||||
|
- const res = await fetch('/api/transcode/session', {
|
||||||
|
+ const res = await fetch('api/transcode/session', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
@@ -345,7 +345,7 @@ class WatchPage {
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[WatchPage] Session start failed:', err);
|
||||||
|
// Fallback to direct transcode if session fails
|
||||||
|
- return `/api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
+ return `api/transcode?url=${encodeURIComponent(url)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -357,7 +357,7 @@ class WatchPage {
|
||||||
|
console.log('[WatchPage] Stopping transcode session:', this.currentSessionId);
|
||||||
|
try {
|
||||||
|
// Fire and forget cleanup
|
||||||
|
- fetch(`/api/transcode/${this.currentSessionId}`, { method: 'DELETE' });
|
||||||
|
+ fetch(`api/transcode/${this.currentSessionId}`, { method: 'DELETE' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to stop session:', err);
|
||||||
|
}
|
||||||
|
@@ -437,7 +437,7 @@ class WatchPage {
|
||||||
|
console.log('[WatchPage] Auto Transcode enabled. Probing stream...');
|
||||||
|
try {
|
||||||
|
const ua = settings.userAgentPreset === 'custom' ? settings.userAgentCustom : settings.userAgentPreset;
|
||||||
|
- const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
|
||||||
|
+ const probeRes = await fetch(`api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
|
||||||
|
const info = await probeRes.json();
|
||||||
|
console.log(`[WatchPage] Probe result: video=${info.video}, audio=${info.audio}, ${info.width}x${info.height}, compatible=${info.compatible}`);
|
||||||
|
|
||||||
|
@@ -470,7 +470,7 @@ class WatchPage {
|
||||||
|
// TODO: Move remux to session logic if seeking is needed for TS files
|
||||||
|
console.log('[WatchPage] Auto: Using remux (.ts container)');
|
||||||
|
this.updateTranscodeStatus('remuxing', 'Remux (Auto)');
|
||||||
|
- const finalUrl = `/api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
+ const finalUrl = `api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
this.video.src = finalUrl;
|
||||||
|
this.video.play().catch(e => {
|
||||||
|
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
|
||||||
|
@@ -509,7 +509,7 @@ class WatchPage {
|
||||||
|
let videoCodec = 'unknown';
|
||||||
|
try {
|
||||||
|
const ua = settings.userAgentPreset === 'custom' ? settings.userAgentCustom : settings.userAgentPreset;
|
||||||
|
- const probeRes = await fetch(`/api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
|
||||||
|
+ const probeRes = await fetch(`api/probe?url=${encodeURIComponent(url)}&ua=${encodeURIComponent(ua || '')}`);
|
||||||
|
const info = await probeRes.json();
|
||||||
|
videoCodec = info.video;
|
||||||
|
} catch (e) { console.warn('Probe failed for force audio, assuming h264'); }
|
||||||
|
@@ -528,7 +528,7 @@ class WatchPage {
|
||||||
|
if (settings.forceRemux && isRawTs) {
|
||||||
|
console.log('[WatchPage] Force Remux enabled');
|
||||||
|
this.updateTranscodeStatus('remuxing', 'Remux (Force)');
|
||||||
|
- const finalUrl = `/api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
+ const finalUrl = `api/remux?url=${encodeURIComponent(url)}`;
|
||||||
|
this.video.src = finalUrl;
|
||||||
|
this.video.play().catch(e => {
|
||||||
|
if (e.name !== 'AbortError') console.error('[WatchPage] Autoplay error:', e);
|
||||||
|
@@ -540,7 +540,7 @@ class WatchPage {
|
||||||
|
// Determine if proxy is needed
|
||||||
|
const proxyRequiredDomains = ['pluto.tv'];
|
||||||
|
const needsProxy = settings.forceProxy || proxyRequiredDomains.some(domain => url.includes(domain));
|
||||||
|
- const finalUrl = needsProxy ? `/api/proxy/stream?url=${encodeURIComponent(url)}` : url;
|
||||||
|
+ const finalUrl = needsProxy ? `api/proxy/stream?url=${encodeURIComponent(url)}` : url;
|
||||||
|
|
||||||
|
console.log('[WatchPage] Playing:', { url, needsProxy, looksLikeHls });
|
||||||
|
|
||||||
|
@@ -600,9 +600,9 @@ class WatchPage {
|
||||||
|
console.error('[WatchPage] HLS fatal error:', data);
|
||||||
|
// Try proxy on CORS error (only if not already proxied/transcoded)
|
||||||
|
// Note: Transcoded streams are local, so no CORS issues usually
|
||||||
|
- if (!url.startsWith('/api/') && (data.type === Hls.ErrorTypes.NETWORK_ERROR)) {
|
||||||
|
+ if (!url.startsWith('api/') && (data.type === Hls.ErrorTypes.NETWORK_ERROR)) {
|
||||||
|
console.log('[WatchPage] Retrying via proxy...');
|
||||||
|
- this.playHls(`/api/proxy/stream?url=${encodeURIComponent(this.currentUrl)}`);
|
||||||
|
+ this.playHls(`api/proxy/stream?url=${encodeURIComponent(this.currentUrl)}`);
|
||||||
|
} else {
|
||||||
|
this.hls.destroy();
|
||||||
|
}
|
||||||
|
@@ -1030,7 +1030,7 @@ class WatchPage {
|
||||||
|
if (!this.content) return;
|
||||||
|
|
||||||
|
const isChannel = this.content.type === 'channel' || !this.content.type; // Default to channel if unknown
|
||||||
|
- const fallback = isChannel ? '/img/placeholder.png' : '/img/poster-placeholder.jpg';
|
||||||
|
+ const fallback = isChannel ? 'img/placeholder.png' : 'img/poster-placeholder.jpg';
|
||||||
|
|
||||||
|
this.posterEl.onerror = () => {
|
||||||
|
this.posterEl.onerror = null;
|
||||||
|
@@ -1130,9 +1130,9 @@ class WatchPage {
|
||||||
|
|
||||||
|
this.recommendedGrid.innerHTML = movies.map(movie => `
|
||||||
|
<div class="watch-recommended-card" data-id="${movie.stream_id}" data-source="${sourceId}">
|
||||||
|
- <img src="${movie.stream_icon || movie.cover || '/img/placeholder.png'}"
|
||||||
|
+ <img src="${movie.stream_icon || movie.cover || 'img/placeholder.png'}"
|
||||||
|
alt="${movie.name}"
|
||||||
|
- onerror="this.onerror=null;this.src='/img/placeholder.png'" loading="lazy">
|
||||||
|
+ onerror="this.onerror=null;this.src='img/placeholder.png'" loading="lazy">
|
||||||
|
<p>${movie.name}</p>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
@@ -1417,7 +1417,7 @@ class WatchPage {
|
||||||
|
currentEpisode: this.currentEpisode || null
|
||||||
|
};
|
||||||
|
|
||||||
|
- await window.API.request('POST', '/history', {
|
||||||
|
+ await window.API.request('POST', 'history', {
|
||||||
|
id: this.content.id,
|
||||||
|
type: this.content.type === 'movie' ? 'movie' : 'episode',
|
||||||
|
sourceId: this.content.sourceId,
|
||||||
|
diff --git a/public/login.html b/public/login.html
|
||||||
|
index 454acfe..34bcb46 100644
|
||||||
|
--- a/public/login.html
|
||||||
|
+++ b/public/login.html
|
||||||
|
@@ -5,7 +5,7 @@
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Login - NodeCast TV</title>
|
||||||
|
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||||
|
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
|
||||||
|
<link rel="stylesheet" href="css/main.css">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
@@ -228,7 +228,7 @@
|
||||||
|
<script>
|
||||||
|
// Start SSO Login
|
||||||
|
document.getElementById('btn-sso-login').addEventListener('click', () => {
|
||||||
|
- window.location.href = '/api/auth/oidc/login';
|
||||||
|
+ window.location.href = 'api/auth/oidc/login';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check for URL Error params
|
||||||
|
@@ -247,7 +247,7 @@
|
||||||
|
|
||||||
|
async function checkSetupRequired() {
|
||||||
|
try {
|
||||||
|
- const response = await fetch('/api/auth/setup-required');
|
||||||
|
+ const response = await fetch('api/auth/setup-required');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.setupRequired) {
|
||||||
|
@@ -283,7 +283,7 @@
|
||||||
|
submitBtn.textContent = 'Please wait...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
- const endpoint = isSetupMode ? '/api/auth/setup' : '/api/auth/login';
|
||||||
|
+ const endpoint = isSetupMode ? 'api/auth/setup' : 'api/auth/login';
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
@@ -302,7 +302,7 @@
|
||||||
|
localStorage.setItem('authToken', data.token);
|
||||||
|
|
||||||
|
// Redirect to main app (replace history to prevent back button issues)
|
||||||
|
- window.location.replace('/');
|
||||||
|
+ window.location.replace('index.html');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
errorMessage.textContent = err.message;
|
||||||
|
@@ -317,4 +317,4 @@
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
-</html>
|
||||||
|
\ No newline at end of file
|
||||||
|
+</html>
|
||||||
+59
-17
@@ -106,30 +106,67 @@ try {
|
|||||||
console.warn('No services directory found or failed to read services:', e.message);
|
console.warn('No services directory found or failed to read services:', e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Freeze services object to prevent plugins from mutating shared state
|
||||||
|
Object.freeze(services);
|
||||||
|
|
||||||
// Plugin loader: loads any .js file inside server/plugins and calls the
|
// Plugin loader: loads any .js file inside server/plugins and calls the
|
||||||
// exported function with (app, services).
|
// exported function with (app, services).
|
||||||
try {
|
// Supports both function exports and object exports with lifecycle hooks.
|
||||||
const pluginsDir = path.join(__dirname, 'plugins');
|
const loadedPlugins = [];
|
||||||
if (fs.existsSync(pluginsDir)) {
|
|
||||||
const pluginFiles = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'));
|
async function loadPlugins() {
|
||||||
for (const file of pluginFiles) {
|
try {
|
||||||
const pluginPath = path.join(pluginsDir, file);
|
const pluginsDir = path.join(__dirname, 'plugins');
|
||||||
try {
|
if (fs.existsSync(pluginsDir)) {
|
||||||
const plugin = require(pluginPath);
|
// Sort plugin files alphabetically for deterministic load order
|
||||||
if (typeof plugin === 'function') {
|
const pluginFiles = fs.readdirSync(pluginsDir)
|
||||||
plugin(app, services);
|
.filter(f => f.endsWith('.js'))
|
||||||
console.log(`Loaded plugin: ${file}`);
|
.sort();
|
||||||
} else {
|
|
||||||
console.warn(`Plugin ${file} does not export a function, skipping.`);
|
for (const file of pluginFiles) {
|
||||||
|
const pluginPath = path.join(pluginsDir, file);
|
||||||
|
try {
|
||||||
|
const plugin = require(pluginPath);
|
||||||
|
|
||||||
|
// Support both function exports and object exports with lifecycle hooks
|
||||||
|
if (typeof plugin === 'function') {
|
||||||
|
// Direct function export (sync or async)
|
||||||
|
await plugin(app, services);
|
||||||
|
loadedPlugins.push({ name: file, plugin: null });
|
||||||
|
console.log(`✓ Loaded plugin: ${file}`);
|
||||||
|
} else if (plugin && typeof plugin.init === 'function') {
|
||||||
|
// Object export with init/shutdown lifecycle
|
||||||
|
await plugin.init(app, services);
|
||||||
|
loadedPlugins.push({ name: file, plugin });
|
||||||
|
console.log(`✓ Loaded plugin: ${file} (with lifecycle hooks)`);
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠ Plugin ${file} does not export a function or object with init(), skipping.`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`✗ Failed to load plugin ${file}:`, err);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Plugin loader failed:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown handler for plugins with shutdown hooks
|
||||||
|
process.on('SIGTERM', async () => {
|
||||||
|
console.log('SIGTERM received, shutting down plugins...');
|
||||||
|
for (const { name, plugin } of loadedPlugins) {
|
||||||
|
if (plugin && typeof plugin.shutdown === 'function') {
|
||||||
|
try {
|
||||||
|
await plugin.shutdown();
|
||||||
|
console.log(`✓ Shutdown plugin: ${name}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Failed to load plugin ${file}:`, err);
|
console.error(`✗ Error shutting down plugin ${name}:`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
process.exit(0);
|
||||||
console.warn('Plugin loader failed:', err.message);
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// API Routes
|
// API Routes
|
||||||
app.use('/api/auth', require('./routes/auth'));
|
app.use('/api/auth', require('./routes/auth'));
|
||||||
@@ -164,6 +201,11 @@ app.use((err, req, res, next) => {
|
|||||||
app.listen(PORT, async () => {
|
app.listen(PORT, async () => {
|
||||||
console.log(`NodeCast TV server running on http://localhost:${PORT}`);
|
console.log(`NodeCast TV server running on http://localhost:${PORT}`);
|
||||||
|
|
||||||
|
// Load plugins
|
||||||
|
await loadPlugins().catch(err => {
|
||||||
|
console.error('Plugin initialization failed:', err);
|
||||||
|
});
|
||||||
|
|
||||||
// Trigger background sync with delay to allow server to settle
|
// Trigger background sync with delay to allow server to settle
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
await syncService.syncAll().catch(console.error);
|
await syncService.syncAll().catch(console.error);
|
||||||
|
|||||||
+147
-7
@@ -6,18 +6,158 @@ This directory allows you to extend the functionality of **NodeCast TV** without
|
|||||||
|
|
||||||
## 🛠️ How It Works
|
## 🛠️ How It Works
|
||||||
|
|
||||||
The plugin loader in `server/index.js` scans this directory and expects each file to export a **initialization function**.
|
The plugin loader in `server/index.js` scans this directory at startup and loads plugins in **alphabetical order** (sorted by filename). Each plugin file should export either:
|
||||||
|
- A function (sync or async) that will be called during initialization
|
||||||
|
- An object with `init()` and optionally `shutdown()` methods for lifecycle management
|
||||||
|
|
||||||
When the server starts, it calls this function and passes the internal Express instance and the loaded services.
|
When the server starts, it calls your plugin's initialization code and passes:
|
||||||
|
- `app` - The Express application instance
|
||||||
|
- `services` - A **frozen** (read-only) object containing all internal services
|
||||||
|
|
||||||
### Plugin Signature
|
---
|
||||||
Each plugin must follow this structure:
|
|
||||||
|
## 📝 Plugin Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Simple Function Export
|
||||||
|
|
||||||
|
The simplest plugin pattern - just export a function:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
/**
|
/**
|
||||||
* @param {Object} app - The Express application instance
|
* @param {Object} app - The Express application instance
|
||||||
* @param {Object} services - Object containing all internal services (db, syncService, etc.)
|
* @param {Object} services - Frozen object containing all internal services
|
||||||
*/
|
*/
|
||||||
module.exports = function(app, services) {
|
module.exports = function(app, services) {
|
||||||
// Your logic here
|
// Register routes, middleware, etc.
|
||||||
};
|
app.get('/api/my-route', (req, res) => {
|
||||||
|
res.json({ message: 'Hello from plugin!' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Async Function Export
|
||||||
|
|
||||||
|
For plugins that need to perform async initialization (database connections, API calls, etc.):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = async function(app, services) {
|
||||||
|
// Async initialization
|
||||||
|
await someAsyncSetup();
|
||||||
|
|
||||||
|
// Access services
|
||||||
|
if (services.syncService) {
|
||||||
|
console.log('Sync service available');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register routes
|
||||||
|
app.get('/api/my-route', (req, res) => {
|
||||||
|
res.json({ status: 'ready' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Lifecycle Hooks (Advanced)
|
||||||
|
|
||||||
|
For plugins that need cleanup on shutdown:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* Called during server startup
|
||||||
|
*/
|
||||||
|
init: async (app, services) => {
|
||||||
|
// Setup code
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
console.log('Background task running...');
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
|
app.get('/api/status', (req, res) => {
|
||||||
|
res.json({ uptime: process.uptime() });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on SIGTERM (graceful shutdown)
|
||||||
|
*/
|
||||||
|
shutdown: async () => {
|
||||||
|
// Cleanup code
|
||||||
|
if (this.interval) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
}
|
||||||
|
console.log('Plugin cleaned up');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Security & Permissions
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> Plugins run with **full Node.js permissions** and have access to the Express app and all services. Only install plugins from trusted sources.
|
||||||
|
|
||||||
|
**Important notes:**
|
||||||
|
- The `services` object is **frozen** - you cannot modify or delete services
|
||||||
|
- Plugins can still mutate properties of service objects (e.g., `services.cache.set()`)
|
||||||
|
- Plugins can register routes, middleware, and access the file system
|
||||||
|
- This is appropriate for self-hosted applications where you control what plugins are installed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Available Services
|
||||||
|
|
||||||
|
The `services` object contains all modules from `server/services/`:
|
||||||
|
|
||||||
|
| Service | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `cache` | Caching utilities |
|
||||||
|
| `epgParser` | EPG/XMLTV parsing |
|
||||||
|
| `hwDetect` | Hardware acceleration detection |
|
||||||
|
| `m3uParser` | M3U playlist parsing |
|
||||||
|
| `m3uXtreamAdapter` | Xtream API adapter |
|
||||||
|
| `syncService` | Channel/EPG synchronization |
|
||||||
|
| `transcodeSession` | Transcoding session management |
|
||||||
|
| `xtreamApi` | Xtream API client |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```javascript
|
||||||
|
module.exports = async function(app, services) {
|
||||||
|
// Use the EPG parser service
|
||||||
|
const epgData = await services.epgParser.fetchAndParse('http://example.com/epg.xml');
|
||||||
|
console.log(`Loaded ${epgData.programmes.length} programmes`);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Load Order
|
||||||
|
|
||||||
|
Plugins are loaded in **alphabetical order** by filename. If you need specific ordering:
|
||||||
|
|
||||||
|
```
|
||||||
|
plugins/
|
||||||
|
01-database.js # Loads first
|
||||||
|
02-api-client.js # Loads second
|
||||||
|
99-cleanup.js # Loads last
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing Your Plugin
|
||||||
|
|
||||||
|
1. Place your `.js` file in `server/plugins/`
|
||||||
|
2. Restart the NodeCast TV server
|
||||||
|
3. Check the console for plugin loading messages:
|
||||||
|
- `✓ Loaded plugin: your-plugin.js` - Success
|
||||||
|
- `⚠ Plugin your-plugin.js does not export...` - Wrong export format
|
||||||
|
- `✗ Failed to load plugin your-plugin.js` - Error during initialization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Example Use Cases
|
||||||
|
|
||||||
|
- **Custom scrapers** - Add support for new streaming sources
|
||||||
|
- **Notification systems** - Send alerts when new content is available
|
||||||
|
- **Analytics** - Track viewing patterns
|
||||||
|
- **Custom APIs** - Expose additional endpoints for integrations
|
||||||
|
- **Middleware** - Add authentication, logging, or rate limiting
|
||||||
+21
-4
@@ -1,8 +1,25 @@
|
|||||||
module.exports = function(app, services) {
|
/**
|
||||||
console.log("🚀 Plugin 'Hello' activé !");
|
* Example plugin demonstrating async initialization and service access
|
||||||
|
* This plugin registers a test route at /api/hello
|
||||||
|
*/
|
||||||
|
module.exports = async function (app, services) {
|
||||||
|
console.log("Plugin 'Hello' activated!");
|
||||||
|
|
||||||
//route de test accessible sur http://localhost:3000/api/hello
|
// Example: Access loaded services
|
||||||
|
if (services.syncService) {
|
||||||
|
console.log(" - syncService is available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate async initialization (e.g., database connection, API setup)
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Register a test route accessible at http://localhost:3000/api/hello
|
||||||
app.get('/api/hello', (req, res) => {
|
app.get('/api/hello', (req, res) => {
|
||||||
res.json({ message: "Le système de plugin fonctionne !" });
|
res.json({
|
||||||
|
message: "The plugin system is working!",
|
||||||
|
availableServices: Object.keys(services)
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(" - Registered route: GET /api/hello");
|
||||||
};
|
};
|
||||||
@@ -266,11 +266,9 @@ async function* fetchAndParseStreaming(url, batchSize = 1000) {
|
|||||||
* @yields {{ channels: Array|null, programmes: Array, isLast: boolean }}
|
* @yields {{ channels: Array|null, programmes: Array, isLast: boolean }}
|
||||||
*/
|
*/
|
||||||
async function* parseStreaming(input, batchSize = 1000) {
|
async function* parseStreaming(input, batchSize = 1000) {
|
||||||
let channels = [];
|
const channels = [];
|
||||||
let programmeBatch = [];
|
let programmeBatch = [];
|
||||||
let channelsYielded = false;
|
let channelsYielded = false;
|
||||||
const maxPenmdingBatches = 4;
|
|
||||||
let paused = false;
|
|
||||||
|
|
||||||
// We need to convert SAX events to an async iterator
|
// We need to convert SAX events to an async iterator
|
||||||
// This requires collecting events and yielding when batch is full
|
// This requires collecting events and yielding when batch is full
|
||||||
@@ -281,7 +279,7 @@ async function* parseStreaming(input, batchSize = 1000) {
|
|||||||
let currentObject = null;
|
let currentObject = null;
|
||||||
let textBuffer = '';
|
let textBuffer = '';
|
||||||
let resolveNext = null;
|
let resolveNext = null;
|
||||||
const pendingBatches = [];
|
let pendingBatch = null;
|
||||||
let ended = false;
|
let ended = false;
|
||||||
let error = null;
|
let error = null;
|
||||||
|
|
||||||
@@ -347,27 +345,13 @@ async function* parseStreaming(input, batchSize = 1000) {
|
|||||||
isLast: false
|
isLast: false
|
||||||
};
|
};
|
||||||
channelsYielded = true;
|
channelsYielded = true;
|
||||||
|
|
||||||
// allow channels array to be GC'd after included in first batch
|
|
||||||
try { if (channels && channels.length) channels = null; } catch (e) {}
|
|
||||||
|
|
||||||
programmeBatch = [];
|
programmeBatch = [];
|
||||||
|
|
||||||
if (resolveNext) {
|
if (resolveNext) {
|
||||||
resolveNext(batch);
|
resolveNext(batch);
|
||||||
resolveNext = null;
|
resolveNext = null;
|
||||||
} else {
|
} else {
|
||||||
pendingBatches.push(batch);
|
pendingBatch = batch;
|
||||||
|
|
||||||
// Apply backpressure when queue grows too large
|
|
||||||
try {
|
|
||||||
if (!paused && pendingBatches.length >= maxPenmdingBatches) {
|
|
||||||
if (input && typeof input.pause === 'function') {
|
|
||||||
input.pause();
|
|
||||||
paused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +398,7 @@ async function* parseStreaming(input, batchSize = 1000) {
|
|||||||
resolveNext(batch);
|
resolveNext(batch);
|
||||||
resolveNext = null;
|
resolveNext = null;
|
||||||
} else {
|
} else {
|
||||||
pendingBatches.push(batch);
|
pendingBatch = batch;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -429,21 +413,10 @@ async function* parseStreaming(input, batchSize = 1000) {
|
|||||||
input.pipe(saxStream);
|
input.pipe(saxStream);
|
||||||
|
|
||||||
// Yield batches as they become available
|
// Yield batches as they become available
|
||||||
|
while (!ended || pendingBatch) {
|
||||||
while (!ended || pendingBatches.length > 0) {
|
if (pendingBatch) {
|
||||||
if (pendingBatches.length > 0) {
|
const batch = pendingBatch;
|
||||||
const batch = pendingBatches.shift();
|
pendingBatch = null;
|
||||||
|
|
||||||
// If paused and queue drained below threshold, resume
|
|
||||||
try {
|
|
||||||
if (paused && pendingBatches.length < maxPenmdingBatches) {
|
|
||||||
if (input && typeof input.resume === 'function') {
|
|
||||||
input.resume();
|
|
||||||
paused = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
yield batch;
|
yield batch;
|
||||||
if (batch.isLast) break;
|
if (batch.isLast) break;
|
||||||
} else if (!ended) {
|
} else if (!ended) {
|
||||||
@@ -472,3 +445,4 @@ module.exports = {
|
|||||||
getProgrammesForChannel,
|
getProgrammesForChannel,
|
||||||
getCurrentAndUpcoming
|
getCurrentAndUpcoming
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
// Test script to verify plugin system improvements
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
console.log('🧪 Testing Plugin System Improvements\n');
|
||||||
|
|
||||||
|
// Test 1: Services freezing
|
||||||
|
console.log('Test 1: Services Object Freezing');
|
||||||
|
const services = {
|
||||||
|
testService: { name: 'test' },
|
||||||
|
anotherService: { value: 123 }
|
||||||
|
};
|
||||||
|
Object.freeze(services);
|
||||||
|
|
||||||
|
try {
|
||||||
|
services.newService = { bad: 'should fail' };
|
||||||
|
console.log('❌ FAIL: Services object is mutable');
|
||||||
|
} catch (e) {
|
||||||
|
console.log('✓ PASS: Cannot add new services (frozen)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// In strict mode, this would throw. In non-strict, it silently fails
|
||||||
|
if (!services.newService) {
|
||||||
|
console.log('✓ PASS: Services object is frozen\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Sorted loading
|
||||||
|
console.log('Test 2: Sorted Plugin Loading');
|
||||||
|
const pluginsDir = path.join(__dirname, 'server', 'plugins');
|
||||||
|
if (fs.existsSync(pluginsDir)) {
|
||||||
|
const pluginFiles = fs.readdirSync(pluginsDir)
|
||||||
|
.filter(f => f.endsWith('.js'))
|
||||||
|
.sort();
|
||||||
|
console.log('Plugin load order:', pluginFiles);
|
||||||
|
console.log('✓ PASS: Plugins sorted alphabetically\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Async function support
|
||||||
|
console.log('Test 3: Async Plugin Support');
|
||||||
|
const asyncPlugin = async (app, services) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 10));
|
||||||
|
return 'async complete';
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const result = await asyncPlugin({}, services);
|
||||||
|
console.log('✓ PASS: Async plugin executed:', result, '\n');
|
||||||
|
|
||||||
|
// Test 4: Lifecycle hooks
|
||||||
|
console.log('Test 4: Lifecycle Hooks Support');
|
||||||
|
const lifecyclePlugin = {
|
||||||
|
init: async (app, services) => {
|
||||||
|
console.log(' - init() called');
|
||||||
|
return 'initialized';
|
||||||
|
},
|
||||||
|
shutdown: async () => {
|
||||||
|
console.log(' - shutdown() called');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof lifecyclePlugin.init === 'function') {
|
||||||
|
await lifecyclePlugin.init({}, services);
|
||||||
|
console.log('✓ PASS: Lifecycle init works');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof lifecyclePlugin.shutdown === 'function') {
|
||||||
|
await lifecyclePlugin.shutdown();
|
||||||
|
console.log('✓ PASS: Lifecycle shutdown works\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ All tests passed!');
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user