wip implementation of local library on anime
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
let animeData = null;
|
||||
let extensionName = null;
|
||||
let animeId = null;
|
||||
let isLocal = false;
|
||||
|
||||
const episodePagination = Object.create(PaginationManager);
|
||||
episodePagination.init(12, renderEpisodes);
|
||||
@@ -13,6 +14,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setupEpisodeSearch();
|
||||
});
|
||||
|
||||
function markAsLocal() {
|
||||
isLocal = true;
|
||||
const pill = document.getElementById('local-pill');
|
||||
if (!pill) return;
|
||||
|
||||
pill.textContent = 'Local';
|
||||
pill.style.display = 'inline-flex';
|
||||
pill.style.background = 'rgba(34,197,94,.2)';
|
||||
pill.style.color = '#22c55e';
|
||||
pill.style.borderColor = 'rgba(34,197,94,.3)';
|
||||
}
|
||||
|
||||
async function checkLocalLibraryEntry() {
|
||||
try {
|
||||
const res = await fetch(`/api/library/anime/${animeId}`);
|
||||
if (!res.ok) return;
|
||||
|
||||
markAsLocal();
|
||||
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loadAnime() {
|
||||
try {
|
||||
|
||||
@@ -24,6 +49,7 @@ async function loadAnime() {
|
||||
|
||||
extensionName = urlData.extensionName;
|
||||
animeId = urlData.entityId;
|
||||
await checkLocalLibraryEntry();
|
||||
|
||||
const fetchUrl = extensionName
|
||||
? `/api/anime/${animeId}?source=${extensionName}`
|
||||
@@ -142,8 +168,8 @@ function setupWatchButton() {
|
||||
const watchBtn = document.getElementById('watch-btn');
|
||||
if (watchBtn) {
|
||||
watchBtn.onclick = () => {
|
||||
const url = URLUtils.buildWatchUrl(animeId, 1, extensionName);
|
||||
window.location.href = url;
|
||||
const source = isLocal ? 'local' : (extensionName || 'anilist');
|
||||
window.location.href = URLUtils.buildWatchUrl(animeId, num, source);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -226,8 +252,8 @@ function createEpisodeButton(num, container) {
|
||||
btn.className = 'episode-btn';
|
||||
btn.innerText = `Ep ${num}`;
|
||||
btn.onclick = () => {
|
||||
const url = URLUtils.buildWatchUrl(animeId, num, extensionName);
|
||||
window.location.href = url;
|
||||
const source = isLocal ? 'local' : (extensionName || 'anilist');
|
||||
window.location.href = URLUtils.buildWatchUrl(animeId, num, source);
|
||||
};
|
||||
container.appendChild(btn);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,28 @@ const firstKey = params.keys().next().value;
|
||||
let extName;
|
||||
if (firstKey) extName = firstKey;
|
||||
|
||||
const href = extName
|
||||
// URL de retroceso: Si es local, volvemos a la vista de Anilist normal
|
||||
const href = (extName && extName !== 'local')
|
||||
? `/anime/${extName}/${animeId}`
|
||||
: `/anime/${animeId}`;
|
||||
|
||||
document.getElementById('back-link').href = href;
|
||||
document.getElementById('episode-label').innerText = `Episode ${currentEpisode}`;
|
||||
|
||||
|
||||
let localEntryId = null;
|
||||
|
||||
async function checkLocal() {
|
||||
try {
|
||||
const res = await fetch(`/api/library/anime/${animeId}`);
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
localEntryId = data.id; // ← ID interna
|
||||
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function loadAniSkip(malId, episode, duration) {
|
||||
try {
|
||||
const res = await fetch(`https://api.aniskip.com/v2/skip-times/${malId}/${episode}?types[]=op&types[]=ed&episodeLength=${duration}`);
|
||||
@@ -37,9 +53,10 @@ async function loadAniSkip(malId, episode, duration) {
|
||||
}
|
||||
|
||||
async function loadMetadata() {
|
||||
checkLocal();
|
||||
try {
|
||||
const extQuery = extName ? `?source=${extName}` : "?source=anilist";
|
||||
const res = await fetch(`/api/anime/${animeId}${extQuery}`);
|
||||
const sourceQuery = (extName === 'local' || !extName) ? "source=anilist" : `source=${extName}`;
|
||||
const res = await fetch(`/api/anime/${animeId}?${sourceQuery}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
@@ -49,13 +66,7 @@ async function loadMetadata() {
|
||||
|
||||
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 title = '', description = '', coverImage = '', averageScore = '', format = '', seasonYear = '', season = '';
|
||||
|
||||
if (isAnilistFormat) {
|
||||
title = data.title.romaji || data.title.english || data.title.native || 'Anime Title';
|
||||
@@ -97,7 +108,8 @@ async function loadMetadata() {
|
||||
document.getElementById('detail-season').innerText = season && seasonYear ? `${season} ${seasonYear}` : (season || seasonYear || '--');
|
||||
document.getElementById('detail-cover-image').src = coverImage || '/default-cover.jpg';
|
||||
|
||||
if (extName) {
|
||||
// Solo cargamos episodios de extensión si hay extensión real y no es local
|
||||
if (extName && extName !== 'local') {
|
||||
await loadExtensionEpisodes();
|
||||
} else {
|
||||
if (data.nextAiringEpisode?.episode) {
|
||||
@@ -109,12 +121,7 @@ async function loadMetadata() {
|
||||
}
|
||||
const simpleEpisodes = [];
|
||||
for (let i = 1; i <= totalEpisodes; i++) {
|
||||
simpleEpisodes.push({
|
||||
number: i,
|
||||
title: null,
|
||||
thumbnail: null,
|
||||
isDub: false
|
||||
});
|
||||
simpleEpisodes.push({ number: i, title: null, thumbnail: null, isDub: false });
|
||||
}
|
||||
populateEpisodeCarousel(simpleEpisodes);
|
||||
}
|
||||
@@ -129,72 +136,30 @@ async function loadMetadata() {
|
||||
}
|
||||
|
||||
async function applyAniSkip(video) {
|
||||
if (!isAnilist || !malId) {
|
||||
console.log('AniSkip disabled: isAnilist=' + isAnilist + ', malId=' + malId);
|
||||
return;
|
||||
}
|
||||
if (!isAnilist || !malId) return;
|
||||
|
||||
console.log('Loading AniSkip for MAL ID:', malId, 'Episode:', currentEpisode);
|
||||
aniSkipData = await loadAniSkip(malId, currentEpisode, Math.floor(video.duration));
|
||||
|
||||
aniSkipData = await loadAniSkip(
|
||||
malId,
|
||||
currentEpisode,
|
||||
Math.floor(video.duration)
|
||||
);
|
||||
if (!aniSkipData || aniSkipData.length === 0) return;
|
||||
|
||||
console.log('AniSkip data received:', aniSkipData);
|
||||
|
||||
if (!aniSkipData || aniSkipData.length === 0) {
|
||||
console.log('No AniSkip data available');
|
||||
return;
|
||||
}
|
||||
|
||||
let op, ed;
|
||||
const markers = [];
|
||||
|
||||
aniSkipData.forEach(item => {
|
||||
const { startTime, endTime } = item.interval;
|
||||
|
||||
if (item.skipType === 'op') {
|
||||
op = { start: startTime, end: endTime };
|
||||
markers.push({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
label: 'Opening'
|
||||
});
|
||||
|
||||
console.log('Opening found:', startTime, '-', endTime);
|
||||
}
|
||||
|
||||
if (item.skipType === 'ed') {
|
||||
ed = { start: startTime, end: endTime };
|
||||
markers.push({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
label: 'Ending'
|
||||
});
|
||||
|
||||
console.log('Ending found:', startTime, '-', endTime);
|
||||
}
|
||||
markers.push({
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
label: item.skipType === 'op' ? 'Opening' : 'Ending'
|
||||
});
|
||||
});
|
||||
|
||||
// Crear markers visuales en el DOM
|
||||
if (plyrInstance && markers.length > 0) {
|
||||
console.log('Creating visual markers:', markers);
|
||||
|
||||
// Esperar a que el player esté completamente cargado
|
||||
setTimeout(() => {
|
||||
const progressContainer = document.querySelector('.plyr__progress');
|
||||
if (!progressContainer) {
|
||||
console.error('Progress container not found');
|
||||
return;
|
||||
}
|
||||
if (!progressContainer) return;
|
||||
|
||||
// Eliminar markers anteriores si existen
|
||||
const oldMarkers = progressContainer.querySelector('.plyr__markers');
|
||||
if (oldMarkers) oldMarkers.remove();
|
||||
|
||||
// Crear contenedor de markers
|
||||
const markersContainer = document.createElement('div');
|
||||
markersContainer.className = 'plyr__markers';
|
||||
|
||||
@@ -216,35 +181,19 @@ async function applyAniSkip(video) {
|
||||
|
||||
markersContainer.appendChild(markerElement);
|
||||
});
|
||||
|
||||
|
||||
progressContainer.appendChild(markersContainer);
|
||||
console.log('Visual markers created successfully');
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadExtensionEpisodes() {
|
||||
try {
|
||||
const extQuery = extName ? `?source=${extName}` : "?source=anilist";
|
||||
const res = await fetch(`/api/anime/${animeId}/episodes${extQuery}`);
|
||||
const res = await fetch(`/api/anime/${animeId}/episodes?source=${extName}`);
|
||||
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);
|
||||
}
|
||||
populateEpisodeCarousel(Array.isArray(data) ? data : []);
|
||||
} catch (e) {
|
||||
console.error("Error cargando episodios por extensión:", e);
|
||||
totalEpisodes = 0;
|
||||
console.error("Error cargando episodios:", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -256,15 +205,12 @@ function populateEpisodeCarousel(episodesData) {
|
||||
const epNumber = ep.number || ep.episodeNumber || ep.id || (index + 1);
|
||||
if (!epNumber) return;
|
||||
|
||||
const extParam = extName ? `?${extName}` : "";
|
||||
const extParam = (extName && extName !== 'local') ? `?${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');
|
||||
@@ -272,21 +218,15 @@ function populateEpisodeCarousel(episodesData) {
|
||||
|
||||
if (hasThumbnail) {
|
||||
const img = document.createElement('img');
|
||||
img.classList.add('carousel-item-img');
|
||||
img.src = ep.thumbnail;
|
||||
img.alt = `Episode ${epNumber} Thumbnail`;
|
||||
img.classList.add('carousel-item-img');
|
||||
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);
|
||||
info.innerHTML = `<p>Ep ${epNumber}: ${ep.title || 'Untitled'}</p>`;
|
||||
link.appendChild(info);
|
||||
carousel.appendChild(link);
|
||||
});
|
||||
@@ -297,28 +237,26 @@ async function loadExtensions() {
|
||||
const res = await fetch('/api/extensions/anime');
|
||||
const data = await res.json();
|
||||
const select = document.getElementById('extension-select');
|
||||
let extensions = data.extensions || [];
|
||||
|
||||
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);
|
||||
});
|
||||
// Añadimos local manualmente
|
||||
if (!extensions.includes('local')) extensions.push('local');
|
||||
|
||||
if (typeof extName === 'string' && data.extensions.includes(extName)) {
|
||||
select.value = extName;
|
||||
} else {
|
||||
select.selectedIndex = 0;
|
||||
}
|
||||
select.innerHTML = '';
|
||||
extensions.forEach(ext => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = opt.innerText = ext;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
currentExtension = select.value;
|
||||
onExtensionChange();
|
||||
if (extName && extensions.includes(extName)) {
|
||||
select.value = extName;
|
||||
} else {
|
||||
select.innerHTML = '<option>No Extensions</option>';
|
||||
select.disabled = true;
|
||||
setLoading("No anime extensions found.");
|
||||
select.value = 'local'; // Default a local
|
||||
}
|
||||
|
||||
currentExtension = select.value;
|
||||
onExtensionChange();
|
||||
} catch (error) {
|
||||
console.error("Extension Error:", error);
|
||||
}
|
||||
@@ -327,83 +265,70 @@ async function loadExtensions() {
|
||||
async function onExtensionChange() {
|
||||
const select = document.getElementById('extension-select');
|
||||
currentExtension = select.value;
|
||||
setLoading("Fetching extension settings...");
|
||||
|
||||
if (currentExtension === 'local') {
|
||||
document.getElementById('sd-toggle').style.display = 'none';
|
||||
document.getElementById('server-select').style.display = 'none';
|
||||
loadStream();
|
||||
return;
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
toggle.style.display = settings.supportsDub ? 'flex' : 'none';
|
||||
setAudioMode('sub');
|
||||
|
||||
const serverSelect = document.getElementById('server-select');
|
||||
serverSelect.innerHTML = '';
|
||||
if (settings.episodeServers && settings.episodeServers.length > 0) {
|
||||
if (settings.episodeServers?.length > 0) {
|
||||
settings.episodeServers.forEach(srv => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = srv;
|
||||
opt.innerText = srv;
|
||||
opt.value = 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.");
|
||||
setLoading("Failed to load 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;
|
||||
|
||||
if (currentExtension === 'local') {
|
||||
console.log(localEntryId);
|
||||
if (!localEntryId) {
|
||||
setLoading("No existe en local");
|
||||
return;
|
||||
}
|
||||
|
||||
const localUrl = `/api/library/stream/anime/${localEntryId}/${currentEpisode}`;
|
||||
playVideo(localUrl, []);
|
||||
document.getElementById('loading-overlay').style.display = 'none';
|
||||
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 sourc = (extName && extName !== 'local') ? `&source=${extName}` : "&source=anilist";
|
||||
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.");
|
||||
if (data.error || !data.videoSources?.length) {
|
||||
setLoading(data.error || "No video sources.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -415,34 +340,31 @@ async function loadStream() {
|
||||
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);
|
||||
playVideo(proxyUrl, source.subtitles || data.subtitles || []);
|
||||
document.getElementById('loading-overlay').style.display = 'none';
|
||||
} catch (error) {
|
||||
setLoading("Stream error. Check console.");
|
||||
console.error(error);
|
||||
setLoading("Stream error.");
|
||||
}
|
||||
}
|
||||
|
||||
function playVideo(url, subtitles = []) {
|
||||
const video = document.getElementById('player');
|
||||
const isLocal = url.includes('/api/library/stream/');
|
||||
|
||||
if (Hls.isSupported()) {
|
||||
if (!isLocal && 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')) {
|
||||
} else {
|
||||
if (hlsInstance) hlsInstance.destroy();
|
||||
video.src = url;
|
||||
}
|
||||
|
||||
if (plyrInstance) plyrInstance.destroy();
|
||||
|
||||
while (video.textTracks.length > 0) {
|
||||
video.removeChild(video.textTracks[0]);
|
||||
}
|
||||
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';
|
||||
@@ -455,118 +377,61 @@ function playVideo(url, subtitles = []) {
|
||||
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'],
|
||||
markers: {
|
||||
enabled: true,
|
||||
points: []
|
||||
}
|
||||
settings: ['captions', 'quality', 'speed']
|
||||
});
|
||||
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
applyAniSkip(video);
|
||||
});
|
||||
video.addEventListener('loadedmetadata', () => applyAniSkip(video));
|
||||
|
||||
// LÓGICA DE RPC (Discord)
|
||||
let rpcActive = false;
|
||||
let lastSeek = 0;
|
||||
|
||||
video.addEventListener("play", () => {
|
||||
if (!video.duration) return;
|
||||
|
||||
const elapsed = Math.floor(video.currentTime);
|
||||
const start = Math.floor(Date.now() / 1000) - elapsed;
|
||||
const end = start + Math.floor(video.duration);
|
||||
|
||||
sendRPC({
|
||||
startTimestamp: start,
|
||||
endTimestamp: end
|
||||
});
|
||||
|
||||
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||
rpcActive = true;
|
||||
});
|
||||
|
||||
video.addEventListener("pause", () => {
|
||||
if (!rpcActive) return;
|
||||
|
||||
sendRPC({
|
||||
paused: true
|
||||
});
|
||||
});
|
||||
|
||||
video.addEventListener("seeking", () => {
|
||||
lastSeek = video.currentTime;
|
||||
if (rpcActive) sendRPC({ paused: true });
|
||||
});
|
||||
|
||||
video.addEventListener("seeked", () => {
|
||||
if (video.paused || !rpcActive) return;
|
||||
|
||||
const elapsed = Math.floor(video.currentTime);
|
||||
const start = Math.floor(Date.now() / 1000) - elapsed;
|
||||
const end = start + Math.floor(video.duration);
|
||||
|
||||
sendRPC({
|
||||
startTimestamp: start,
|
||||
endTimestamp: end
|
||||
});
|
||||
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||
});
|
||||
|
||||
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
||||
fetch("/api/rpc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
details: animeTitle,
|
||||
state: `Episode ${currentEpisode}`,
|
||||
mode: "watching",
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
paused
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(message) {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
const text = document.getElementById('loading-text');
|
||||
overlay.style.display = 'flex';
|
||||
text.innerText = message;
|
||||
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
||||
fetch("/api/rpc", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
details: animeTitle,
|
||||
state: `Episode ${currentEpisode}`,
|
||||
mode: "watching",
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
paused
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
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 source = (extName && extName !== 'local') ? extName : "anilist";
|
||||
|
||||
const body = {
|
||||
entry_id: animeId,
|
||||
source: source,
|
||||
entry_type: "ANIME",
|
||||
status: 'CURRENT',
|
||||
progress: source === 'anilist'
|
||||
? Math.floor(currentEpisode)
|
||||
: currentEpisode
|
||||
progress: currentEpisode
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -583,7 +448,39 @@ async function sendProgress() {
|
||||
}
|
||||
}
|
||||
|
||||
// Botones y Toggle
|
||||
document.getElementById('sd-toggle').onclick = () => {
|
||||
audioMode = audioMode === 'sub' ? 'dub' : 'sub';
|
||||
setAudioMode(audioMode);
|
||||
loadStream();
|
||||
};
|
||||
|
||||
function setAudioMode(mode) {
|
||||
const toggle = document.getElementById('sd-toggle');
|
||||
toggle.setAttribute('data-state', mode);
|
||||
document.getElementById('opt-sub').classList.toggle('active', mode === 'sub');
|
||||
document.getElementById('opt-dub').classList.toggle('active', mode === 'dub');
|
||||
}
|
||||
|
||||
function setLoading(message) {
|
||||
document.getElementById('loading-text').innerText = message;
|
||||
document.getElementById('loading-overlay').style.display = 'flex';
|
||||
}
|
||||
|
||||
const extParam = (extName && extName !== 'local') ? `?${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;
|
||||
|
||||
// Actualizar progreso cada 1 minuto si el video está reproduciéndose
|
||||
setInterval(() => {
|
||||
if (plyrInstance && !plyrInstance.paused) sendProgress();
|
||||
}, 60000);
|
||||
|
||||
loadMetadata();
|
||||
loadExtensions();
|
||||
|
||||
loadExtensions();
|
||||
209
desktop/src/scripts/local-library.js
Normal file
209
desktop/src/scripts/local-library.js
Normal file
@@ -0,0 +1,209 @@
|
||||
let activeFilter = 'all';
|
||||
let activeSort = 'az';
|
||||
let isLocalMode = false;
|
||||
let localEntries = [];
|
||||
|
||||
function toggleLibraryMode() {
|
||||
isLocalMode = !isLocalMode;
|
||||
|
||||
const btn = document.getElementById('library-mode-btn');
|
||||
const onlineContent = document.getElementById('online-content');
|
||||
const localContent = document.getElementById('local-content');
|
||||
const svg = btn.querySelector('svg');
|
||||
const label = btn.querySelector('span');
|
||||
|
||||
if (isLocalMode) {
|
||||
// LOCAL MODE
|
||||
btn.classList.add('active');
|
||||
onlineContent.classList.add('hidden');
|
||||
localContent.classList.remove('hidden');
|
||||
loadLocalEntries();
|
||||
|
||||
svg.innerHTML = `
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
`;
|
||||
} else {
|
||||
// ONLINE MODE
|
||||
btn.classList.remove('active');
|
||||
onlineContent.classList.remove('hidden');
|
||||
localContent.classList.add('hidden');
|
||||
|
||||
svg.innerHTML = `
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocalEntries() {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
grid.innerHTML = '<div class="skeleton-card"></div>'.repeat(8);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/library/anime');
|
||||
const entries = await response.json();
|
||||
localEntries = entries;
|
||||
|
||||
if (entries.length === 0) {
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-text-secondary); padding: 3rem;">No anime found in your local library. Click "Scan Library" to scan your folders.</p>';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Renderizar grid
|
||||
grid.innerHTML = entries.map(entry => {
|
||||
const title = entry.metadata?.title?.romaji || entry.metadata?.title?.english || entry.id;
|
||||
const cover = entry.metadata?.coverImage?.extraLarge || entry.metadata?.coverImage?.large || '/public/assets/placeholder.jpg';
|
||||
const score = entry.metadata?.averageScore || '--';
|
||||
const episodes = entry.metadata?.episodes || '??';
|
||||
|
||||
return `
|
||||
<div class="local-card" onclick="viewLocalEntry(${entry.metadata?.id || 'null'})">
|
||||
<div class="card-img-wrap">
|
||||
<img src="${cover}" alt="${title}" loading="lazy">
|
||||
</div>
|
||||
<div class="local-card-info">
|
||||
<div class="local-card-title">${title}</div>
|
||||
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
|
||||
${score}% • ${episodes} Eps
|
||||
</p>
|
||||
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
|
||||
${entry.matched ? '● Linked' : '○ Unlinked'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} catch (err) {
|
||||
console.error('Error loading local entries:', err);
|
||||
grid.innerHTML = '<p style="grid-column: 1/-1; text-align: center; color: var(--color-danger); padding: 3rem;">Error loading local library. Make sure the backend is running.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function scanLocalLibrary() {
|
||||
const btnText = document.getElementById('scan-text');
|
||||
const originalText = btnText.innerText;
|
||||
btnText.innerText = "Scanning...";
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/library/scan?mode=incremental', {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadLocalEntries();
|
||||
// Mostrar notificación de éxito si tienes sistema de notificaciones
|
||||
if (window.NotificationUtils) {
|
||||
NotificationUtils.show('Library scanned successfully!', 'success');
|
||||
}
|
||||
} else {
|
||||
throw new Error('Scan failed');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Scan failed", err);
|
||||
alert("Failed to scan library. Check console for details.");
|
||||
|
||||
// Mostrar notificación de error si tienes sistema de notificaciones
|
||||
if (window.NotificationUtils) {
|
||||
NotificationUtils.show('Failed to scan library', 'error');
|
||||
}
|
||||
} finally {
|
||||
btnText.innerText = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function viewLocalEntry(anilistId) {
|
||||
if (!anilistId) {
|
||||
console.warn('Anime not linked');
|
||||
return;
|
||||
}
|
||||
window.location.href = `/anime/${anilistId}`;
|
||||
}
|
||||
|
||||
function renderLocalEntries(entries) {
|
||||
const grid = document.getElementById('local-entries-grid');
|
||||
|
||||
grid.innerHTML = entries.map(entry => {
|
||||
const title = entry.metadata?.title?.romaji
|
||||
|| entry.metadata?.title?.english
|
||||
|| entry.id;
|
||||
|
||||
const cover =
|
||||
entry.metadata?.coverImage?.extraLarge
|
||||
|| entry.metadata?.coverImage?.large
|
||||
|| '/public/assets/placeholder.jpg';
|
||||
|
||||
const score = entry.metadata?.averageScore || '--';
|
||||
const episodes = entry.metadata?.episodes || '??';
|
||||
|
||||
return `
|
||||
<div class="local-card" onclick="viewLocalEntry(${entry.metadata?.id || 'null'})">
|
||||
<div class="card-img-wrap">
|
||||
<img src="${cover}" alt="${title}" loading="lazy">
|
||||
</div>
|
||||
<div class="local-card-info">
|
||||
<div class="local-card-title">${title}</div>
|
||||
<p style="font-size: 0.85rem; color: var(--color-text-secondary); margin: 0;">
|
||||
${score}% • ${episodes} Eps
|
||||
</p>
|
||||
<div class="match-status ${entry.matched ? 'status-linked' : 'status-unlinked'}">
|
||||
${entry.matched ? '● Linked' : '○ Unlinked'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function applyLocalFilters() {
|
||||
let filtered = [...localEntries];
|
||||
|
||||
if (activeFilter === 'linked') {
|
||||
filtered = filtered.filter(e => e.matched);
|
||||
}
|
||||
|
||||
if (activeFilter === 'unlinked') {
|
||||
filtered = filtered.filter(e => !e.matched);
|
||||
}
|
||||
|
||||
if (activeSort === 'az') {
|
||||
filtered.sort((a, b) =>
|
||||
(a.metadata?.title?.romaji || a.id)
|
||||
.localeCompare(b.metadata?.title?.romaji || b.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (activeSort === 'za') {
|
||||
filtered.sort((a, b) =>
|
||||
(b.metadata?.title?.romaji || b.id)
|
||||
.localeCompare(a.metadata?.title?.romaji || a.id)
|
||||
);
|
||||
}
|
||||
|
||||
renderLocalEntries(filtered);
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const btn = e.target.closest('.filter-btn');
|
||||
if (!btn) return;
|
||||
|
||||
if (btn.dataset.filter) {
|
||||
activeFilter = btn.dataset.filter;
|
||||
}
|
||||
|
||||
if (btn.dataset.sort) {
|
||||
activeSort = btn.dataset.sort;
|
||||
}
|
||||
|
||||
btn
|
||||
.closest('.local-filters')
|
||||
.querySelectorAll('.filter-btn')
|
||||
.forEach(b => b.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
|
||||
applyLocalFilters();
|
||||
});
|
||||
Reference in New Issue
Block a user