Organized the differences between server and docker versions.
We are launching a docker version (server version) today so we want to just organize the repo so its easier to navigate.
This commit is contained in:
301
desktop/src/scripts/anime/anime.js
Normal file
301
desktop/src/scripts/anime/anime.js
Normal file
@@ -0,0 +1,301 @@
|
||||
let animeData = null;
|
||||
let extensionName = null;
|
||||
let animeId = null;
|
||||
|
||||
const episodePagination = Object.create(PaginationManager);
|
||||
episodePagination.init(12, renderEpisodes);
|
||||
|
||||
YouTubePlayerUtils.init('player');
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadAnime();
|
||||
setupDescriptionModal();
|
||||
setupEpisodeSearch();
|
||||
});
|
||||
|
||||
async function loadAnime() {
|
||||
try {
|
||||
|
||||
const urlData = URLUtils.parseEntityPath('anime');
|
||||
if (!urlData) {
|
||||
showError("Invalid URL");
|
||||
return;
|
||||
}
|
||||
|
||||
extensionName = urlData.extensionName;
|
||||
animeId = urlData.entityId;
|
||||
|
||||
const fetchUrl = extensionName
|
||||
? `/api/anime/${animeId}?source=${extensionName}`
|
||||
: `/api/anime/${animeId}?source=anilist`;
|
||||
|
||||
const res = await fetch(fetchUrl, { headers: AuthUtils.getSimpleAuthHeaders() });
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
showError("Anime Not Found");
|
||||
return;
|
||||
}
|
||||
|
||||
animeData = data;
|
||||
|
||||
const metadata = MediaMetadataUtils.formatAnimeData(data, !!extensionName);
|
||||
|
||||
updatePageTitle(metadata.title);
|
||||
updateMetadata(metadata);
|
||||
updateDescription(data.description || data.summary);
|
||||
updateCharacters(metadata.characters);
|
||||
updateExtensionPill();
|
||||
|
||||
setupWatchButton();
|
||||
|
||||
const hasTrailer = YouTubePlayerUtils.playTrailer(
|
||||
metadata.trailer,
|
||||
'player',
|
||||
metadata.banner
|
||||
);
|
||||
|
||||
setupEpisodes(metadata.episodes);
|
||||
|
||||
await setupAddToListButton();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading anime:', err);
|
||||
showError("Error loading anime");
|
||||
}
|
||||
}
|
||||
|
||||
function updatePageTitle(title) {
|
||||
document.title = `${title} | WaifuBoard`;
|
||||
document.getElementById('title').innerText = title;
|
||||
}
|
||||
|
||||
function updateMetadata(metadata) {
|
||||
|
||||
if (metadata.poster) {
|
||||
document.getElementById('poster').src = metadata.poster;
|
||||
}
|
||||
|
||||
document.getElementById('score').innerText = `${metadata.score}% Score`;
|
||||
|
||||
document.getElementById('year').innerText = metadata.year;
|
||||
|
||||
document.getElementById('genres').innerText = metadata.genres;
|
||||
|
||||
document.getElementById('format').innerText = metadata.format;
|
||||
|
||||
document.getElementById('status').innerText = metadata.status;
|
||||
|
||||
document.getElementById('season').innerText = metadata.season;
|
||||
|
||||
document.getElementById('studio').innerText = metadata.studio;
|
||||
|
||||
document.getElementById('episodes').innerText = metadata.episodes;
|
||||
}
|
||||
|
||||
function updateDescription(rawDescription) {
|
||||
const desc = MediaMetadataUtils.truncateDescription(rawDescription, 4);
|
||||
|
||||
document.getElementById('description-preview').innerHTML = desc.short;
|
||||
document.getElementById('full-description').innerHTML = desc.full;
|
||||
|
||||
const readMoreBtn = document.getElementById('read-more-btn');
|
||||
if (desc.isTruncated) {
|
||||
readMoreBtn.style.display = 'inline-flex';
|
||||
} else {
|
||||
readMoreBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function updateCharacters(characters) {
|
||||
const container = document.getElementById('char-list');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (characters.length > 0) {
|
||||
characters.forEach(char => {
|
||||
container.innerHTML += `
|
||||
<div class="character-item">
|
||||
<div class="char-dot"></div> ${char.name}
|
||||
</div>`;
|
||||
});
|
||||
} else {
|
||||
container.innerHTML = `
|
||||
<div class="character-item" style="color: #666;">
|
||||
No character data available
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function updateExtensionPill() {
|
||||
const pill = document.getElementById('extension-pill');
|
||||
if (!pill) return;
|
||||
|
||||
if (extensionName) {
|
||||
pill.textContent = extensionName.charAt(0).toUpperCase() + extensionName.slice(1).toLowerCase();
|
||||
pill.style.display = 'inline-flex';
|
||||
} else {
|
||||
pill.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setupWatchButton() {
|
||||
const watchBtn = document.getElementById('watch-btn');
|
||||
if (watchBtn) {
|
||||
watchBtn.onclick = () => {
|
||||
const url = URLUtils.buildWatchUrl(animeId, 1, extensionName);
|
||||
window.location.href = url;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAddToListButton() {
|
||||
const btn = document.getElementById('add-to-list-btn');
|
||||
if (!btn || !animeData) return;
|
||||
|
||||
ListModalManager.currentData = animeData;
|
||||
const entryType = ListModalManager.getEntryType(animeData);
|
||||
|
||||
await ListModalManager.checkIfInList(animeId, extensionName || 'anilist', entryType);
|
||||
|
||||
const tempBtn = document.querySelector('.hero-buttons .btn-blur');
|
||||
if (tempBtn) {
|
||||
ListModalManager.updateButton('.hero-buttons .btn-blur');
|
||||
} else {
|
||||
|
||||
updateCustomAddButton();
|
||||
}
|
||||
|
||||
btn.onclick = () => ListModalManager.open(animeData, extensionName || 'anilist');
|
||||
}
|
||||
|
||||
function updateCustomAddButton() {
|
||||
const btn = document.getElementById('add-to-list-btn');
|
||||
if (!btn) return;
|
||||
|
||||
if (ListModalManager.isInList) {
|
||||
btn.innerHTML = `
|
||||
<svg width="20" height="20" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/>
|
||||
</svg>
|
||||
In Your List
|
||||
`;
|
||||
btn.style.background = 'rgba(34, 197, 94, 0.2)';
|
||||
btn.style.color = '#22c55e';
|
||||
btn.style.borderColor = 'rgba(34, 197, 94, 0.3)';
|
||||
} else {
|
||||
btn.innerHTML = '+ Add to List';
|
||||
btn.style.background = null;
|
||||
btn.style.color = null;
|
||||
btn.style.borderColor = null;
|
||||
}
|
||||
}
|
||||
|
||||
function setupEpisodes(totalEpisodes) {
|
||||
|
||||
const limitedTotal = Math.min(Math.max(totalEpisodes, 1), 5000);
|
||||
|
||||
episodePagination.setTotalItems(limitedTotal);
|
||||
renderEpisodes();
|
||||
}
|
||||
|
||||
function renderEpisodes() {
|
||||
const grid = document.getElementById('episodes-grid');
|
||||
if (!grid) return;
|
||||
|
||||
grid.innerHTML = '';
|
||||
|
||||
const range = episodePagination.getPageRange();
|
||||
const start = range.start + 1;
|
||||
|
||||
const end = range.end;
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
createEpisodeButton(i, grid);
|
||||
}
|
||||
|
||||
episodePagination.renderControls(
|
||||
'pagination-controls',
|
||||
'page-info',
|
||||
'prev-page',
|
||||
'next-page'
|
||||
);
|
||||
}
|
||||
|
||||
function createEpisodeButton(num, container) {
|
||||
const btn = document.createElement('div');
|
||||
btn.className = 'episode-btn';
|
||||
btn.innerText = `Ep ${num}`;
|
||||
btn.onclick = () => {
|
||||
const url = URLUtils.buildWatchUrl(animeId, num, extensionName);
|
||||
window.location.href = url;
|
||||
};
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
function setupDescriptionModal() {
|
||||
const modal = document.getElementById('desc-modal');
|
||||
if (!modal) return;
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target.id === 'desc-modal') {
|
||||
closeDescriptionModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openDescriptionModal() {
|
||||
document.getElementById('desc-modal').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closeDescriptionModal() {
|
||||
document.getElementById('desc-modal').classList.remove('active');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
function setupEpisodeSearch() {
|
||||
const searchInput = document.getElementById('ep-search');
|
||||
if (!searchInput) return;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value);
|
||||
const grid = document.getElementById('episodes-grid');
|
||||
const totalEpisodes = episodePagination.totalItems;
|
||||
|
||||
if (val > 0 && val <= totalEpisodes) {
|
||||
grid.innerHTML = '';
|
||||
createEpisodeButton(val, grid);
|
||||
document.getElementById('pagination-controls').style.display = 'none';
|
||||
} else if (!e.target.value) {
|
||||
renderEpisodes();
|
||||
} else {
|
||||
grid.innerHTML = '<div style="color:#666; width:100%; text-align:center;">Episode not found</div>';
|
||||
document.getElementById('pagination-controls').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('title').innerText = message;
|
||||
}
|
||||
|
||||
function saveToList() {
|
||||
if (!animeId) return;
|
||||
ListModalManager.save(animeId, extensionName || 'anilist');
|
||||
}
|
||||
|
||||
function deleteFromList() {
|
||||
if (!animeId) return;
|
||||
ListModalManager.delete(animeId, extensionName || 'anilist');
|
||||
}
|
||||
|
||||
function closeAddToListModal() {
|
||||
ListModalManager.close();
|
||||
}
|
||||
|
||||
window.openDescriptionModal = openDescriptionModal;
|
||||
window.closeDescriptionModal = closeDescriptionModal;
|
||||
window.changePage = (delta) => {
|
||||
if (delta > 0) episodePagination.nextPage();
|
||||
else episodePagination.prevPage();
|
||||
};
|
||||
194
desktop/src/scripts/anime/animes.js
Normal file
194
desktop/src/scripts/anime/animes.js
Normal file
@@ -0,0 +1,194 @@
|
||||
let trendingAnimes = [];
|
||||
let currentHeroIndex = 0;
|
||||
let player;
|
||||
let heroInterval;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
SearchManager.init('#search-input', '#search-results', 'anime');
|
||||
ContinueWatchingManager.load('my-status', 'watching', 'ANIME');
|
||||
fetchContent();
|
||||
setInterval(() => fetchContent(true), 60000);
|
||||
});
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.search-wrapper')) {
|
||||
const searchResults = document.getElementById('search-results');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
searchResults.classList.remove('active');
|
||||
searchInput.style.borderRadius = '99px';
|
||||
}
|
||||
});
|
||||
|
||||
function scrollCarousel(id, direction) {
|
||||
const container = document.getElementById(id);
|
||||
if(container) {
|
||||
const scrollAmount = container.clientWidth * 0.75;
|
||||
container.scrollBy({ left: direction * scrollAmount, behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
var tag = document.createElement('script');
|
||||
tag.src = "https://www.youtube.com/iframe_api";
|
||||
var firstScriptTag = document.getElementsByTagName('script')[0];
|
||||
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||
|
||||
function onYouTubeIframeAPIReady() {
|
||||
player = new YT.Player('player', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
playerVars: {
|
||||
'autoplay': 1,
|
||||
'controls': 0,
|
||||
'mute': 1,
|
||||
'loop': 1,
|
||||
'showinfo': 0,
|
||||
'modestbranding': 1
|
||||
},
|
||||
events: {
|
||||
'onReady': (e) => {
|
||||
e.target.mute();
|
||||
if(trendingAnimes.length) updateHeroVideo(trendingAnimes[currentHeroIndex]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchContent(isUpdate = false) {
|
||||
try {
|
||||
const trendingRes = await fetch('/api/trending');
|
||||
const trendingData = await trendingRes.json();
|
||||
|
||||
if (trendingData.results && trendingData.results.length > 0) {
|
||||
trendingAnimes = trendingData.results;
|
||||
if (!isUpdate) {
|
||||
updateHeroUI(trendingAnimes[0]);
|
||||
startHeroCycle();
|
||||
}
|
||||
renderList('trending', trendingAnimes);
|
||||
} else if (!isUpdate) {
|
||||
setTimeout(() => fetchContent(false), 2000);
|
||||
}
|
||||
|
||||
const topRes = await fetch('/api/top-airing');
|
||||
const topData = await topRes.json();
|
||||
if (topData.results && topData.results.length > 0) {
|
||||
renderList('top-airing', topData.results);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fetch Error:", e);
|
||||
if(!isUpdate) setTimeout(() => fetchContent(false), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
function startHeroCycle() {
|
||||
if(heroInterval) clearInterval(heroInterval);
|
||||
heroInterval = setInterval(() => {
|
||||
if(trendingAnimes.length > 0) {
|
||||
currentHeroIndex = (currentHeroIndex + 1) % trendingAnimes.length;
|
||||
updateHeroUI(trendingAnimes[currentHeroIndex]);
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
async function updateHeroUI(anime) {
|
||||
if(!anime) return;
|
||||
|
||||
const title = anime.title.english || anime.title.romaji || "Unknown Title";
|
||||
const score = anime.averageScore ? anime.averageScore + '% Match' : 'N/A';
|
||||
const year = anime.seasonYear || '';
|
||||
const type = anime.format || 'TV';
|
||||
const desc = anime.description || 'No description available.';
|
||||
const poster = anime.coverImage ? anime.coverImage.extraLarge : '';
|
||||
const banner = anime.bannerImage || poster;
|
||||
|
||||
document.getElementById('hero-title').innerText = title;
|
||||
document.getElementById('hero-desc').innerHTML = desc;
|
||||
document.getElementById('hero-score').innerText = score;
|
||||
document.getElementById('hero-year').innerText = year;
|
||||
document.getElementById('hero-type').innerText = type;
|
||||
document.getElementById('hero-poster').src = poster;
|
||||
|
||||
const watchBtn = document.getElementById('watch-btn');
|
||||
if(watchBtn) watchBtn.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||||
|
||||
const addToListBtn = document.querySelector('.hero-buttons .btn-blur');
|
||||
if(addToListBtn) {
|
||||
ListModalManager.currentData = anime;
|
||||
const entryType = ListModalManager.getEntryType(anime);
|
||||
|
||||
await ListModalManager.checkIfInList(anime.id, 'anilist', entryType);
|
||||
ListModalManager.updateButton();
|
||||
|
||||
addToListBtn.onclick = () => ListModalManager.open(anime, 'anilist');
|
||||
}
|
||||
|
||||
const bgImg = document.getElementById('hero-bg-media');
|
||||
if(bgImg && bgImg.src !== banner) bgImg.src = banner;
|
||||
|
||||
updateHeroVideo(anime);
|
||||
|
||||
document.getElementById('hero-loading-ui').style.display = 'none';
|
||||
document.getElementById('hero-real-ui').style.display = 'block';
|
||||
}
|
||||
|
||||
function updateHeroVideo(anime) {
|
||||
if (!player || !player.loadVideoById) return;
|
||||
const videoContainer = document.getElementById('player');
|
||||
if (anime.trailer && anime.trailer.site === 'youtube' && anime.trailer.id) {
|
||||
if(player.getVideoData && player.getVideoData().video_id !== anime.trailer.id) {
|
||||
player.loadVideoById(anime.trailer.id);
|
||||
player.mute();
|
||||
}
|
||||
videoContainer.style.opacity = "1";
|
||||
} else {
|
||||
videoContainer.style.opacity = "0";
|
||||
player.stopVideo();
|
||||
}
|
||||
}
|
||||
|
||||
function renderList(id, list) {
|
||||
const container = document.getElementById(id);
|
||||
const firstId = list.length > 0 ? list[0].id : null;
|
||||
const currentFirstId = container.firstElementChild?.dataset?.id;
|
||||
if (currentFirstId && parseInt(currentFirstId) === firstId && container.children.length === list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
list.forEach(anime => {
|
||||
const title = anime.title.english || anime.title.romaji || "Unknown Title";
|
||||
const cover = anime.coverImage ? anime.coverImage.large : '';
|
||||
const ep = anime.nextAiringEpisode ? 'Ep ' + anime.nextAiringEpisode.episode : (anime.episodes ? anime.episodes + ' Eps' : 'TV');
|
||||
const score = anime.averageScore || '--';
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = 'card';
|
||||
el.dataset.id = anime.id;
|
||||
|
||||
el.onclick = () => window.location.href = `/anime/${anime.id}`;
|
||||
el.innerHTML = `
|
||||
<div class="card-img-wrap"><img src="${cover}" loading="lazy"></div>
|
||||
<div class="card-content">
|
||||
<h3>${title}</h3>
|
||||
<p>${score}% • ${ep}</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function saveToList() {
|
||||
const animeId = ListModalManager.currentData ? ListModalManager.currentData.id : null;
|
||||
if (!animeId) return;
|
||||
ListModalManager.save(animeId, 'anilist');
|
||||
}
|
||||
|
||||
function deleteFromList() {
|
||||
const animeId = ListModalManager.currentData ? ListModalManager.currentData.id : null;
|
||||
if (!animeId) return;
|
||||
ListModalManager.delete(animeId, 'anilist');
|
||||
}
|
||||
|
||||
function closeAddToListModal() {
|
||||
ListModalManager.close();
|
||||
}
|
||||
431
desktop/src/scripts/anime/player.js
Normal file
431
desktop/src/scripts/anime/player.js
Normal file
@@ -0,0 +1,431 @@
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const animeId = pathParts[2];
|
||||
const currentEpisode = parseInt(pathParts[3]);
|
||||
|
||||
let audioMode = 'sub';
|
||||
let currentExtension = '';
|
||||
let plyrInstance;
|
||||
let hlsInstance;
|
||||
let totalEpisodes = 0;
|
||||
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const firstKey = params.keys().next().value;
|
||||
let extName;
|
||||
if (firstKey) extName = firstKey;
|
||||
|
||||
const href = extName
|
||||
? `/anime/${extName}/${animeId}`
|
||||
: `/anime/${animeId}`;
|
||||
|
||||
document.getElementById('back-link').href = href;
|
||||
document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`;
|
||||
|
||||
async function loadMetadata() {
|
||||
try {
|
||||
const extQuery = extName ? `?source=${extName}` : "?source=anilist";
|
||||
const res = await fetch(`/api/anime/${animeId}${extQuery}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
console.error("Error from API:", data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const isAnilistFormat = data.title && (data.title.romaji || data.title.english);
|
||||
|
||||
let title = '';
|
||||
let description = '';
|
||||
let coverImage = '';
|
||||
let averageScore = '';
|
||||
let format = '';
|
||||
let seasonYear = '';
|
||||
let season = '';
|
||||
let episodesCount = 0;
|
||||
let characters = [];
|
||||
|
||||
if (isAnilistFormat) {
|
||||
|
||||
title = data.title.romaji || data.title.english || data.title.native || 'Anime Title';
|
||||
description = data.description || 'No description available.';
|
||||
coverImage = data.coverImage?.large || data.coverImage?.medium || '';
|
||||
averageScore = data.averageScore ? `${data.averageScore}%` : '--';
|
||||
format = data.format || '--';
|
||||
season = data.season ? data.season.charAt(0) + data.season.slice(1).toLowerCase() : '';
|
||||
seasonYear = data.seasonYear || '';
|
||||
} else {
|
||||
title = data.title || 'Anime Title';
|
||||
description = data.summary || 'No description available.';
|
||||
coverImage = data.image || '';
|
||||
averageScore = data.score ? `${Math.round(data.score * 10)}%` : '--';
|
||||
format = '--';
|
||||
season = data.season || '';
|
||||
seasonYear = data.year || '';
|
||||
}
|
||||
|
||||
document.getElementById('anime-title-details').innerText = title;
|
||||
document.getElementById('anime-title-details2').innerText = title;
|
||||
document.title = `Watching ${title} - Ep ${currentEpisode}`;
|
||||
|
||||
fetch("/api/rpc", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
details: title,
|
||||
state: `Episode ${currentEpisode}`,
|
||||
mode: "watching"
|
||||
})
|
||||
});
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = description;
|
||||
document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText || 'No description available.';
|
||||
|
||||
document.getElementById('detail-format').innerText = format;
|
||||
document.getElementById('detail-score').innerText = averageScore;
|
||||
document.getElementById('detail-season').innerText = season && seasonYear ? `${season} ${seasonYear}` : (season || seasonYear || '--');
|
||||
document.getElementById('detail-cover-image').src = coverImage || '/default-cover.jpg';
|
||||
|
||||
if (extName) {
|
||||
await loadExtensionEpisodes();
|
||||
} else {
|
||||
if (data.nextAiringEpisode?.episode) {
|
||||
totalEpisodes = data.nextAiringEpisode.episode - 1;
|
||||
} else if (data.episodes) {
|
||||
totalEpisodes = data.episodes;
|
||||
} else {
|
||||
totalEpisodes = 12;
|
||||
}
|
||||
const simpleEpisodes = [];
|
||||
for (let i = 1; i <= totalEpisodes; i++) {
|
||||
simpleEpisodes.push({
|
||||
number: i,
|
||||
title: null,
|
||||
thumbnail: null,
|
||||
isDub: false
|
||||
});
|
||||
}
|
||||
populateEpisodeCarousel(simpleEpisodes);
|
||||
}
|
||||
|
||||
if (currentEpisode >= totalEpisodes && totalEpisodes > 0) {
|
||||
document.getElementById('next-btn').disabled = true;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading metadata:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExtensionEpisodes() {
|
||||
try {
|
||||
const extQuery = extName ? `?source=${extName}` : "?source=anilist";
|
||||
const res = await fetch(`/api/anime/${animeId}/episodes${extQuery}`);
|
||||
const data = await res.json();
|
||||
|
||||
totalEpisodes = Array.isArray(data) ? data.length : 0;
|
||||
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
populateEpisodeCarousel(data);
|
||||
} else {
|
||||
|
||||
const fallback = [];
|
||||
for (let i = 1; i <= totalEpisodes; i++) {
|
||||
fallback.push({ number: i, title: null, thumbnail: null });
|
||||
}
|
||||
populateEpisodeCarousel(fallback);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error cargando episodios por extensión:", e);
|
||||
totalEpisodes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function populateEpisodeCarousel(episodesData) {
|
||||
const carousel = document.getElementById('episode-carousel');
|
||||
carousel.innerHTML = '';
|
||||
|
||||
episodesData.forEach((ep, index) => {
|
||||
const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1);
|
||||
if (!epNumber) return;
|
||||
|
||||
const extParam = extName ? `?${extName}` : "";
|
||||
const hasThumbnail = ep.thumbnail && ep.thumbnail.trim() !== '';
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = `/watch/${animeId}/${epNumber}${extParam}`;
|
||||
link.classList.add('carousel-item');
|
||||
link.dataset.episode = epNumber;
|
||||
|
||||
if (!hasThumbnail) link.classList.add('no-thumbnail');
|
||||
if (parseInt(epNumber) === currentEpisode) link.classList.add('active-ep-carousel');
|
||||
|
||||
const imgContainer = document.createElement('div');
|
||||
imgContainer.classList.add('carousel-item-img-container');
|
||||
|
||||
if (hasThumbnail) {
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('carousel-item-img');
|
||||
img.src = ep.thumbnail;
|
||||
img.alt = `Episode ${epNumber} Thumbnail`;
|
||||
imgContainer.appendChild(img);
|
||||
}
|
||||
|
||||
link.appendChild(imgContainer);
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.classList.add('carousel-item-info');
|
||||
|
||||
const title = document.createElement('p');
|
||||
title.innerText = `Ep ${epNumber}: ${ep.title || 'Untitled'}`;
|
||||
|
||||
info.appendChild(title);
|
||||
link.appendChild(info);
|
||||
carousel.appendChild(link);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadExtensions() {
|
||||
try {
|
||||
const res = await fetch('/api/extensions/anime');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('extension-select');
|
||||
|
||||
if (data.extensions && data.extensions.length > 0) {
|
||||
select.innerHTML = '';
|
||||
data.extensions.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = opt.innerText = ext;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
if (typeof extName === 'string' && data.extensions.includes(extName)) {
|
||||
select.value = extName;
|
||||
} else {
|
||||
select.selectedIndex = 0;
|
||||
}
|
||||
|
||||
currentExtension = select.value;
|
||||
onExtensionChange();
|
||||
} else {
|
||||
select.innerHTML = '<option>No Extensions</option>';
|
||||
select.disabled = true;
|
||||
setLoading("No anime extensions found.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Extension Error:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function onExtensionChange() {
|
||||
const select = document.getElementById('extension-select');
|
||||
currentExtension = select.value;
|
||||
setLoading("Fetching extension settings...");
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/extensions/${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 (error) {
|
||||
console.error(error);
|
||||
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);
|
||||
subOpt.classList.toggle('active', mode === 'sub');
|
||||
dubOpt.classList.toggle('active', mode === 'dub');
|
||||
}
|
||||
|
||||
async function loadStream() {
|
||||
if (!currentExtension) return;
|
||||
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
const server = serverSelect.value || "default";
|
||||
|
||||
setLoading(`Loading stream (${audioMode})...`);
|
||||
|
||||
try {
|
||||
let sourc = "&source=anilist";
|
||||
if (extName){
|
||||
sourc = `&source=${extName}`;
|
||||
}
|
||||
const url = `/api/watch/stream?animeId=${animeId}&episode=${currentEpisode}&server=${server}&category=${audioMode}&ext=${currentExtension}${sourc}`;
|
||||
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];
|
||||
const headers = data.headers || {};
|
||||
|
||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.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 || data.subtitles);
|
||||
document.getElementById('loading-overlay').style.display = 'none';
|
||||
} catch (error) {
|
||||
setLoading("Stream error. Check console.");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function playVideo(url, subtitles = []) {
|
||||
const video = document.getElementById('player');
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
if (hlsInstance) hlsInstance.destroy();
|
||||
hlsInstance = new Hls({ xhrSetup: (xhr) => xhr.withCredentials = false });
|
||||
hlsInstance.loadSource(url);
|
||||
hlsInstance.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
video.src = url;
|
||||
}
|
||||
|
||||
if (plyrInstance) plyrInstance.destroy();
|
||||
|
||||
while (video.textTracks.length > 0) {
|
||||
video.removeChild(video.textTracks[0]);
|
||||
}
|
||||
|
||||
subtitles.forEach(sub => {
|
||||
if (!sub.url) return;
|
||||
const track = document.createElement('track');
|
||||
track.kind = 'captions';
|
||||
track.label = sub.language || 'Unknown';
|
||||
track.srclang = (sub.language || '').slice(0, 2).toLowerCase();
|
||||
track.src = sub.url;
|
||||
if (sub.default || sub.language?.toLowerCase().includes('english')) track.default = true;
|
||||
video.appendChild(track);
|
||||
});
|
||||
|
||||
plyrInstance = new Plyr(video, {
|
||||
captions: { active: true, update: true, language: 'en' },
|
||||
controls: ['play-large', 'play', 'progress', 'current-time', 'duration', 'mute', 'volume', 'captions', 'settings', 'pip', 'airplay', 'fullscreen'],
|
||||
settings: ['captions', 'quality', 'speed']
|
||||
});
|
||||
|
||||
let alreadyTriggered = false;
|
||||
|
||||
video.addEventListener('timeupdate', () => {
|
||||
if (!video.duration) return;
|
||||
|
||||
const percent = (video.currentTime / video.duration) * 100;
|
||||
|
||||
if (percent >= 80 && !alreadyTriggered) {
|
||||
alreadyTriggered = true;
|
||||
sendProgress();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
video.play().catch(() => console.log("Autoplay blocked"));
|
||||
}
|
||||
|
||||
function setLoading(message) {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
const text = document.getElementById('loading-text');
|
||||
overlay.style.display = 'flex';
|
||||
text.innerText = message;
|
||||
}
|
||||
|
||||
const extParam = extName ? `?${extName}` : "";
|
||||
|
||||
document.getElementById('prev-btn').onclick = () => {
|
||||
if (currentEpisode > 1) {
|
||||
window.location.href = `/watch/${animeId}/${currentEpisode - 1}${extParam}`;
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('next-btn').onclick = () => {
|
||||
if (currentEpisode < totalEpisodes || totalEpisodes === 0) {
|
||||
window.location.href = `/watch/${animeId}/${currentEpisode + 1}${extParam}`;
|
||||
}
|
||||
};
|
||||
|
||||
if (currentEpisode <= 1) {
|
||||
document.getElementById('prev-btn').disabled = true;
|
||||
}
|
||||
|
||||
|
||||
async function sendProgress() {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) return;
|
||||
|
||||
const source = extName
|
||||
? extName
|
||||
: "anilist";
|
||||
|
||||
const body = {
|
||||
entry_id: animeId,
|
||||
source: source,
|
||||
entry_type: "ANIME",
|
||||
status: 'CURRENT',
|
||||
progress: source === 'anilist'
|
||||
? Math.floor(currentEpisode)
|
||||
: currentEpisode
|
||||
};
|
||||
|
||||
try {
|
||||
await fetch('/api/list/entry', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error updating progress:', err);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
loadMetadata();
|
||||
loadExtensions();
|
||||
|
||||
Reference in New Issue
Block a user