handle application/vnd.apple.mpegurl.
This commit is contained in:
126
frontend/app.js
126
frontend/app.js
@@ -4,16 +4,23 @@ const perPage = 12;
|
||||
const renderedVideoIds = new Set();
|
||||
let hasNextPage = true;
|
||||
let isLoading = false;
|
||||
let hlsPlayer = null;
|
||||
|
||||
// 2. Observer Definition (Must be defined before initApp uses it)
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
if (entries[0].isIntersecting) loadVideos();
|
||||
}, { threshold: 1.0 });
|
||||
}, {
|
||||
threshold: 1.0
|
||||
});
|
||||
|
||||
// 3. Logic Functions
|
||||
async function InitializeLocalStorage() {
|
||||
if (!localStorage.getItem('config')) {
|
||||
localStorage.setItem('config', JSON.stringify({ servers: [{ "https://getfigleaf.com": {} }] }));
|
||||
localStorage.setItem('config', JSON.stringify({
|
||||
servers: [{
|
||||
"https://getfigleaf.com": {}
|
||||
}]
|
||||
}));
|
||||
}
|
||||
if (!localStorage.getItem('theme')) {
|
||||
localStorage.setItem('theme', 'dark');
|
||||
@@ -31,13 +38,20 @@ async function InitializeServerStatus() {
|
||||
try {
|
||||
const response = await fetch(`/api/status`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ server: server }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
server: server
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
});
|
||||
const status = await response.json();
|
||||
serverObj[server] = status;
|
||||
} catch (err) {
|
||||
serverObj[server] = { online: false, channels: [] };
|
||||
serverObj[server] = {
|
||||
online: false,
|
||||
channels: []
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,7 +64,7 @@ async function InitializeServerStatus() {
|
||||
if (serverData.channels && serverData.channels.length > 0) {
|
||||
const channel = serverData.channels[0];
|
||||
let options = {};
|
||||
|
||||
|
||||
if (channel.options) {
|
||||
channel.options.forEach(element => {
|
||||
// Ensure the options structure matches your API expectations
|
||||
@@ -74,7 +88,7 @@ async function InitializeServerStatus() {
|
||||
|
||||
async function loadVideos() {
|
||||
const session = JSON.parse(localStorage.getItem('session'));
|
||||
if (!session) return;
|
||||
if (!session) return;
|
||||
if (isLoading || !hasNextPage) return;
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
@@ -92,7 +106,7 @@ async function loadVideos() {
|
||||
// Correct way to loop through the options object
|
||||
Object.entries(session.options).forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
body[key] = value.map((entry) => entry.id);
|
||||
body[key] = value.map((entry) => entry.id).join(", ");
|
||||
} else if (value && value.id) {
|
||||
body[key] = value.id;
|
||||
}
|
||||
@@ -102,7 +116,9 @@ async function loadVideos() {
|
||||
isLoading = true;
|
||||
const response = await fetch('/api/videos', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const videos = await response.json();
|
||||
@@ -124,7 +140,7 @@ function renderVideos(videos) {
|
||||
const items = videos && Array.isArray(videos.items) ? videos.items : [];
|
||||
items.forEach(v => {
|
||||
if (renderedVideoIds.has(v.id)) return;
|
||||
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'video-card';
|
||||
const durationText = v.duration === 0 ? '' : ` • ${v.duration}s`;
|
||||
@@ -144,10 +160,10 @@ async function initApp() {
|
||||
// Clear old data if you want a fresh start every refresh
|
||||
// localStorage.clear();
|
||||
|
||||
await InitializeLocalStorage();
|
||||
await InitializeLocalStorage();
|
||||
applyTheme();
|
||||
renderMenu();
|
||||
|
||||
|
||||
const sentinel = document.getElementById('sentinel');
|
||||
if (sentinel) {
|
||||
observer.observe(sentinel);
|
||||
@@ -163,10 +179,57 @@ function applyTheme() {
|
||||
if (select) select.value = theme;
|
||||
}
|
||||
|
||||
function openPlayer(url) {
|
||||
async function openPlayer(url) {
|
||||
const modal = document.getElementById('video-modal');
|
||||
const video = document.getElementById('player');
|
||||
video.src = `/api/stream?url=${encodeURIComponent(url)}`;
|
||||
|
||||
// 1. Define isHls (the missing piece!)
|
||||
const streamUrl = `/api/stream?url=${encodeURIComponent(url)}`;
|
||||
let isHls = /\.m3u8($|\?)/i.test(url);
|
||||
|
||||
// 2. Cleanup existing player instance to prevent aborted bindings
|
||||
if (hlsPlayer) {
|
||||
hlsPlayer.stopLoad();
|
||||
hlsPlayer.detachMedia();
|
||||
hlsPlayer.destroy();
|
||||
hlsPlayer = null;
|
||||
}
|
||||
|
||||
// 3. Reset the video element
|
||||
video.pause();
|
||||
video.removeAttribute('src');
|
||||
video.load();
|
||||
|
||||
if (!isHls) {
|
||||
try {
|
||||
const headResp = await fetch(streamUrl, { method: 'HEAD' });
|
||||
const contentType = headResp.headers.get('Content-Type') || '';
|
||||
if (contentType.includes('application/vnd.apple.mpegurl')) {
|
||||
isHls = true;
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to detect stream type', err);
|
||||
}
|
||||
}
|
||||
|
||||
if (isHls) {
|
||||
if (window.Hls && window.Hls.isSupported()) {
|
||||
hlsPlayer = new window.Hls();
|
||||
hlsPlayer.loadSource(streamUrl);
|
||||
hlsPlayer.attachMedia(video);
|
||||
hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() {
|
||||
video.play();
|
||||
});
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = streamUrl;
|
||||
} else {
|
||||
console.error("HLS not supported in this browser.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
video.src = streamUrl;
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
@@ -174,6 +237,10 @@ function openPlayer(url) {
|
||||
function closePlayer() {
|
||||
const modal = document.getElementById('video-modal');
|
||||
const video = document.getElementById('player');
|
||||
if (hlsPlayer) {
|
||||
hlsPlayer.destroy();
|
||||
hlsPlayer = null;
|
||||
}
|
||||
video.pause();
|
||||
video.src = '';
|
||||
modal.style.display = 'none';
|
||||
@@ -190,7 +257,9 @@ function handleSearch(value) {
|
||||
}
|
||||
|
||||
function getConfig() {
|
||||
return JSON.parse(localStorage.getItem('config')) || { servers: [] };
|
||||
return JSON.parse(localStorage.getItem('config')) || {
|
||||
servers: []
|
||||
};
|
||||
}
|
||||
|
||||
function getSession() {
|
||||
@@ -206,7 +275,10 @@ function getServerEntries() {
|
||||
if (!config.servers || !Array.isArray(config.servers)) return [];
|
||||
return config.servers.map((serverObj) => {
|
||||
const server = Object.keys(serverObj)[0];
|
||||
return { url: server, data: serverObj[server] || null };
|
||||
return {
|
||||
url: server,
|
||||
data: serverObj[server] || null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,9 +352,9 @@ function renderMenu() {
|
||||
sourceSelect.onchange = () => {
|
||||
const selectedServerUrl = sourceSelect.value;
|
||||
const selectedServer = serverEntries.find((entry) => entry.url === selectedServerUrl);
|
||||
const channels = selectedServer && selectedServer.data && selectedServer.data.channels
|
||||
? selectedServer.data.channels
|
||||
: [];
|
||||
const channels = selectedServer && selectedServer.data && selectedServer.data.channels ?
|
||||
selectedServer.data.channels :
|
||||
[];
|
||||
const nextChannel = channels.length > 0 ? channels[0] : null;
|
||||
const nextSession = {
|
||||
server: selectedServerUrl,
|
||||
@@ -295,9 +367,9 @@ function renderMenu() {
|
||||
};
|
||||
|
||||
const activeServer = serverEntries.find((entry) => entry.url === (session && session.server));
|
||||
const availableChannels = activeServer && activeServer.data && activeServer.data.channels
|
||||
? activeServer.data.channels
|
||||
: [];
|
||||
const availableChannels = activeServer && activeServer.data && activeServer.data.channels ?
|
||||
activeServer.data.channels :
|
||||
[];
|
||||
|
||||
channelSelect.innerHTML = "";
|
||||
availableChannels.forEach((channel) => {
|
||||
@@ -389,7 +461,9 @@ function renderMenu() {
|
||||
const exists = (config.servers || []).some((serverObj) => Object.keys(serverObj)[0] === normalized);
|
||||
if (!exists) {
|
||||
config.servers = config.servers || [];
|
||||
config.servers.push({ [normalized]: {} });
|
||||
config.servers.push({
|
||||
[normalized]: {}
|
||||
});
|
||||
setConfig(config);
|
||||
sourceInput.value = '';
|
||||
await refreshServerStatus();
|
||||
@@ -398,9 +472,9 @@ function renderMenu() {
|
||||
if (!session || session.server !== normalized) {
|
||||
const entries = getServerEntries();
|
||||
const addedEntry = entries.find((entry) => entry.url === normalized);
|
||||
const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels
|
||||
? addedEntry.data.channels[0]
|
||||
: null;
|
||||
const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels ?
|
||||
addedEntry.data.channels[0] :
|
||||
null;
|
||||
setSession({
|
||||
server: normalized,
|
||||
channel: nextChannel,
|
||||
|
||||
Reference in New Issue
Block a user