First dev release of v2.0.0

This commit is contained in:
2025-11-26 15:10:53 -05:00
commit 638b2d9fc4
18 changed files with 7093 additions and 0 deletions

296
views/watch.html Normal file
View File

@@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>StreamFlow Watch</title>
<link rel="stylesheet" href="../public/watch.css">
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
<script src="https://cdn.plyr.io/3.7.8/plyr.js"></script>
</head>
<body>
<div class="top-bar">
<a href="#" id="back-link" class="back-btn">
<svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
Back to Series
</a>
</div>
<div class="theater-container">
<!-- Toolbar -->
<div class="player-toolbar">
<div class="sd-toggle" id="sd-toggle" data-state="sub" style="display: none;" onclick="toggleAudioMode()">
<div class="sd-bg"></div>
<div class="sd-option active" id="opt-sub">Sub</div>
<div class="sd-option" id="opt-dub">Dub</div>
</div>
<select id="server-select" class="extension-select" style="display:none;" onchange="loadStream()">
<!-- Populated via JS -->
</select>
<select id="extension-select" class="extension-select" onchange="onExtensionChange()">
<option value="" disabled selected>Source...</option>
</select>
</div>
<!-- Video Player -->
<div class="video-wrapper">
<video id="player" controls crossorigin playsinline poster=""></video>
<div id="loading-overlay" class="loading-overlay">
<div class="spinner"></div>
<span id="loading-text">Select a source...</span>
</div>
</div>
<!-- Controls -->
<div class="controls-area">
<div class="episode-info">
<h2 id="anime-title">Loading...</h2>
<span id="episode-label">Episode --</span>
</div>
<div class="nav-controls">
<button class="nav-btn" id="prev-btn">
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 19l-7-7 7-7"/></svg>
Prev
</button>
<button class="nav-btn" id="next-btn">
Next
<svg width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 5l7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
<script>
const pathParts = window.location.pathname.split('/');
const animeId = pathParts[2];
const currentEpisode = parseInt(pathParts[3]);
let audioMode = 'sub';
let currentExtension = '';
let plyrInstance;
let hlsInstance;
document.getElementById('back-link').href = `/anime/${animeId}`;
document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`;
async function loadMetadata() {
try {
const res = await fetch(`/api/anime/${animeId}`);
const data = await res.json();
if(!data.error) {
const title = data.title.english || data.title.romaji;
document.getElementById('anime-title').innerText = title;
document.title = `Watching ${title} - Ep ${currentEpisode}`;
}
} catch(e) { console.error(e); }
}
async function loadExtensions() {
try {
const res = await fetch('/api/extensions');
const data = await res.json();
const select = document.getElementById('extension-select');
if (data.extensions && data.extensions.length > 0) {
data.extensions.forEach(extName => {
const opt = document.createElement('option');
opt.value = extName;
opt.innerText = extName;
select.appendChild(opt);
});
} else {
select.innerHTML = '<option>No Extensions</option>';
select.disabled = true;
setLoading("No extensions found in WaifuBoards folder.");
}
} catch(e) { console.error("Extension Error:", e); }
}
async function onExtensionChange() {
const select = document.getElementById('extension-select');
currentExtension = select.value;
setLoading("Fetching extension settings...");
try {
const res = await fetch(`/api/extension/${currentExtension}/settings`);
const settings = await res.json();
const toggle = document.getElementById('sd-toggle');
if (settings.supportsDub) {
toggle.style.display = 'flex';
setAudioMode('sub');
} else {
toggle.style.display = 'none';
setAudioMode('sub');
}
const serverSelect = document.getElementById('server-select');
serverSelect.innerHTML = '';
if (settings.episodeServers && settings.episodeServers.length > 0) {
settings.episodeServers.forEach(srv => {
const opt = document.createElement('option');
opt.value = srv;
opt.innerText = srv;
serverSelect.appendChild(opt);
});
serverSelect.style.display = 'block';
} else {
serverSelect.style.display = 'none';
}
loadStream();
} catch (err) {
console.error(err);
setLoading("Failed to load extension settings.");
}
}
function toggleAudioMode() {
const newMode = audioMode === 'sub' ? 'dub' : 'sub';
setAudioMode(newMode);
loadStream();
}
function setAudioMode(mode) {
audioMode = mode;
const toggle = document.getElementById('sd-toggle');
const subOpt = document.getElementById('opt-sub');
const dubOpt = document.getElementById('opt-dub');
toggle.setAttribute('data-state', mode);
if (mode === 'sub') {
subOpt.classList.add('active');
dubOpt.classList.remove('active');
} else {
subOpt.classList.remove('active');
dubOpt.classList.add('active');
}
}
async function loadStream() {
if (!currentExtension) return;
const serverSelect = document.getElementById('server-select');
const server = serverSelect.value || "default";
setLoading(`Searching & Resolving Stream (${audioMode})...`);
try {
const url = `/api/watch/stream?animeId=${animeId}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}`;
const res = await fetch(url);
const data = await res.json();
if (data.error) {
setLoading(`Error: ${data.error}`);
return;
}
if (!data.videoSources || data.videoSources.length === 0) {
setLoading("No video sources found.");
return;
}
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
// --- THE PROXY URL CONTRUCTION ---
const headers = data.headers || {};
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
// Append headers to proxy URL
if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
if (headers['Origin']) proxyUrl += `&origin=${encodeURIComponent(headers['Origin'])}`;
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
playVideo(proxyUrl, data.videoSources[0].subtitles);
document.getElementById('loading-overlay').style.display = 'none';
} catch (err) {
setLoading("Stream Error. Check console.");
console.error(err);
}
}
function playVideo(url, subtitles) {
const video = document.getElementById('player');
if (Hls.isSupported()) {
if (hlsInstance) hlsInstance.destroy();
hlsInstance = new Hls({
xhrSetup: (xhr, url) => {
xhr.withCredentials = false; // Ensure no cookies are sent to proxy
}
});
hlsInstance.loadSource(url);
hlsInstance.attachMedia(video);
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
video.src = url;
}
// Ensure clean slate
if (plyrInstance) plyrInstance.destroy();
// 1. Define Tracks
// We attach them to the DOM or Plyr configuration so Plyr recognizes them.
// The 'captions' option in Plyr can also take a 'src' via config if we were not using HLS.js directly attached.
// However, since we use HLS.js attached to the video tag, Plyr wraps that video tag.
// Clear existing tracks
while (video.firstChild) {
video.removeChild(video.firstChild);
}
// Add Track Elements
if (subtitles && subtitles.length > 0) {
subtitles.forEach(sub => {
const track = document.createElement('track');
track.kind = 'captions';
track.label = sub.language;
track.srclang = sub.language.slice(0, 2).toLowerCase();
track.src = sub.url; // These are usually VTT/SRT links
if (sub.default || sub.language.toLowerCase().includes('english')) {
track.default = true;
}
video.appendChild(track);
});
}
// 2. Initialize Plyr
plyrInstance = new Plyr(video, {
captions: { active: true, update: true, language: 'en' }, // Enable by default
controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
settings: ['captions', 'quality', 'speed']
});
video.play().catch(e => console.log("Auto-play blocked"));
}
function setLoading(msg) {
const overlay = document.getElementById('loading-overlay');
const text = document.getElementById('loading-text');
overlay.style.display = 'flex';
text.innerText = msg;
}
document.getElementById('prev-btn').onclick = () => {
if(currentEpisode > 1) window.location.href = `/watch/${animeId}/${currentEpisode - 1}`;
};
document.getElementById('next-btn').onclick = () => {
window.location.href = `/watch/${animeId}/${currentEpisode + 1}`;
};
if(currentEpisode <= 1) document.getElementById('prev-btn').disabled = true;
loadMetadata();
loadExtensions();
</script>
</body>
</html>