Files
Nodecast/public/js/api.js
blomios 4cca030afa feat: split live analysis actions and page catalog loading
Separate incremental live checks from full catalog rebuilds and page projected channel/variant loading so first live-list display stays bounded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-07 23:45:42 +02:00

219 lines
10 KiB
JavaScript

/**
* API Client - Frontend API wrapper for NodeCast TV
*/
const API = {
/**
* Make API request
*/
async request(method, endpoint, data = null) {
const options = {
method,
headers: {
'Content-Type': 'application/json'
}
};
// Add authentication token if available
const token = localStorage.getItem('authToken');
if (token) {
options.headers['Authorization'] = `Bearer ${token}`;
}
if (data) {
options.body = JSON.stringify(data);
}
const response = await fetch(`/api${endpoint}`, options);
let result;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
result = await response.json();
} else {
const text = await response.text();
result = { error: text || 'API request failed' };
}
if (!response.ok) {
// If unauthorized, redirect to login
if (response.status === 401) {
localStorage.removeItem('authToken');
window.location.href = '/login.html';
return;
}
throw new Error(result.error || `Server responded with ${response.status}`);
}
return result;
},
// 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
updateEpgOrder: (orderedIds) => API.request('PUT', '/sources/epg/order', { orderedIds }),
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)
},
cast: {
getDevices: () => API.request('GET', '/cast/devices'),
devices: () => API.request('GET', '/cast/devices'),
refreshDevices: () => API.request('POST', '/cast/devices/refresh'),
startSession: (payload) => API.request('POST', '/cast/sessions', payload),
createSession: (payload) => API.request('POST', '/cast/sessions', payload),
getSessions: () => API.request('GET', '/cast/sessions'),
getSession: (id) => API.request('GET', `/cast/sessions/${encodeURIComponent(id)}`),
stopSession: (id) => API.request('DELETE', `/cast/sessions/${encodeURIComponent(id)}`)
},
// 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 }),
// 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 })
},
live: {
getChannels: ({ sourceId = null, includeHidden = false, q = '', languages = [], limit = 100, offset = 0 } = {}) => {
const params = new URLSearchParams();
if (sourceId) params.set('sourceId', sourceId);
params.set('includeHidden', String(Boolean(includeHidden)));
if (q) params.set('q', q);
if (languages.length) params.set('languages', languages.join(','));
params.set('limit', String(limit));
params.set('offset', String(offset));
params.set('page', String(Math.floor(offset / limit) + 1));
return API.request('GET', `/live/channels?${params.toString()}`);
},
getChannelVariants: (logicalChannelId, { includeHidden = false, limit = 100, offset = 0 } = {}) => {
const params = new URLSearchParams({
includeHidden: String(Boolean(includeHidden)),
limit: String(limit),
offset: String(offset),
page: String(Math.floor(offset / limit) + 1)
});
return API.request('GET', `/live/channels/${encodeURIComponent(logicalChannelId)}/variants?${params.toString()}`);
},
checkNewStreams: () => API.request('POST', '/live/projection/check-new'),
rebuildProjection: () => API.request('POST', '/live/projection/rebuild'),
getProjectionStatus: () => API.request('GET', '/live/projection/status')
},
// Favorites
favorites: {
getAll: (sourceId = null, itemType = null) => {
let url = '/favorites';
const params = [];
if (sourceId) params.push(`sourceId=${sourceId}`);
if (itemType) params.push(`itemType=${itemType}`);
if (params.length) url += '?' + params.join('&');
return API.request('GET', url);
},
add: (sourceId, itemId, itemType = 'channel') =>
API.request('POST', '/favorites', { sourceId, itemId, itemType }),
remove: (sourceId, itemId, itemType = 'channel') =>
API.request('DELETE', '/favorites', { sourceId, itemId, itemType }),
check: (sourceId, itemId, itemType = 'channel') =>
API.request('GET', `/favorites/check?sourceId=${sourceId}&itemId=${itemId}&itemType=${itemType}`)
},
laya: {
getStatus: () => API.request('GET', '/laya/status'),
downloadModel: (model = 'multilingual') => API.request('POST', '/laya/model/download', { model }),
removeModel: () => API.request('DELETE', '/laya/model'),
detectLanguages: (items, model = 'multilingual') => API.request('POST', '/laya/language/batch', { items, model })
},
// Proxy
proxy: {
// Xtream
xtream: {
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}`);
},
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}`);
},
vodCategories: (sourceId, options = {}) => {
const params = options.includeHidden ? '?includeHidden=true' : '';
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}`);
},
seriesCategories: (sourceId, options = {}) => {
const params = options.includeHidden ? '?includeHidden=true' : '';
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}`);
},
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}`),
getStreamUrl: (sourceId, streamId, type = 'live', container = 'm3u8') =>
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 })
},
// Cache management
cache: {
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')
},
// 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}`)
}
};
// Make API available globally
window.API = API;