added open in mpv on anime for electron ver
This commit is contained in:
@@ -116,7 +116,6 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
|
|||||||
|
|
||||||
export async function openInMPV(req: any, reply: any) {
|
export async function openInMPV(req: any, reply: any) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const { title, video, subtitles = [], chapters = [], animeId, episode, entrySource, token } = req.body;
|
const { title, video, subtitles = [], chapters = [], animeId, episode, entrySource, token } = req.body;
|
||||||
|
|
||||||
if (!video?.url) return { error: 'Missing video url' };
|
if (!video?.url) return { error: 'Missing video url' };
|
||||||
@@ -132,7 +131,7 @@ export async function openInMPV(req: any, reply: any) {
|
|||||||
`&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`;
|
`&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`;
|
||||||
|
|
||||||
const proxySubs = subtitles.map((s: any) =>
|
const proxySubs = subtitles.map((s: any) =>
|
||||||
`${proxyBase}?url=${encodeURIComponent(s.url)}` +
|
`${proxyBase}?url=${encodeURIComponent(s.src)}` +
|
||||||
`&referer=${encodeURIComponent(video.headers?.Referer ?? '')}` +
|
`&referer=${encodeURIComponent(video.headers?.Referer ?? '')}` +
|
||||||
`&origin=${encodeURIComponent(video.headers?.Origin ?? '')}` +
|
`&origin=${encodeURIComponent(video.headers?.Origin ?? '')}` +
|
||||||
`&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`
|
`&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`
|
||||||
@@ -142,20 +141,26 @@ export async function openInMPV(req: any, reply: any) {
|
|||||||
|
|
||||||
let chaptersArg: string[] = [];
|
let chaptersArg: string[] = [];
|
||||||
if (chapters.length) {
|
if (chapters.length) {
|
||||||
chapters.sort((a: any, b: any) => a.interval.startTime - b.interval.startTime);
|
|
||||||
|
chapters.sort((a: any, b: any) => a.startTime - b.startTime);
|
||||||
|
|
||||||
const lines = [';FFMETADATA1'];
|
const lines = [';FFMETADATA1'];
|
||||||
|
|
||||||
for (let i = 0; i < chapters.length; i++) {
|
for (let i = 0; i < chapters.length; i++) {
|
||||||
const c = chapters[i];
|
const c = chapters[i];
|
||||||
const start = Math.floor(c.interval.startTime * 1000);
|
|
||||||
const end = Math.floor(c.interval.endTime * 1000);
|
const start = Math.floor(c.startTime * 1000);
|
||||||
|
const end = Math.floor(c.endTime * 1000);
|
||||||
|
|
||||||
|
const title = (c.type || 'chapter').toUpperCase();
|
||||||
|
|
||||||
lines.push(
|
lines.push(
|
||||||
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${start}`, `END=${end}`, `title=${c.skipType.toUpperCase()}`
|
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${start}`, `END=${end}`, `title=${title}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (i < chapters.length - 1) {
|
if (i < chapters.length - 1) {
|
||||||
const nextStart = Math.floor(chapters[i + 1].interval.startTime * 1000);
|
const nextStart = Math.floor(chapters[i + 1].startTime * 1000);
|
||||||
|
|
||||||
if (nextStart - end > 1000) {
|
if (nextStart - end > 1000) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${end}`, `END=${nextStart}`, `title=Episode`
|
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${end}`, `END=${nextStart}`, `title=Episode`
|
||||||
@@ -293,9 +298,17 @@ export async function openInMPV(req: any, reply: any) {
|
|||||||
|
|
||||||
commands.forEach(cmd => socket.write(JSON.stringify(cmd) + '\n'));
|
commands.forEach(cmd => socket.write(JSON.stringify(cmd) + '\n'));
|
||||||
|
|
||||||
for (const sub of proxySubs) {
|
subtitles.forEach((s: any, i: number) => {
|
||||||
socket.write(JSON.stringify({ command: ['sub-add', sub, 'auto'] }) + '\n');
|
socket.write(JSON.stringify({
|
||||||
}
|
command: [
|
||||||
|
'sub-add',
|
||||||
|
proxySubs[i],
|
||||||
|
'auto',
|
||||||
|
s.label || 'Subtitle',
|
||||||
|
s.srclang || ''
|
||||||
|
]
|
||||||
|
}) + '\n');
|
||||||
|
});
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ const AnimePlayer = (function() {
|
|||||||
let _skipIntervals = [];
|
let _skipIntervals = [];
|
||||||
let _progressUpdated = false;
|
let _progressUpdated = false;
|
||||||
|
|
||||||
// Variables nuevas para RPC
|
|
||||||
let _animeTitle = "Anime";
|
let _animeTitle = "Anime";
|
||||||
let _rpcActive = false;
|
let _rpcActive = false;
|
||||||
|
|
||||||
|
let _rawVideoData = null;
|
||||||
|
let _currentSubtitles = [];
|
||||||
|
|
||||||
let _localEntryId = null;
|
let _localEntryId = null;
|
||||||
let _totalEpisodes = 0;
|
let _totalEpisodes = 0;
|
||||||
|
|
||||||
@@ -31,7 +33,8 @@ const AnimePlayer = (function() {
|
|||||||
subDubToggle: null,
|
subDubToggle: null,
|
||||||
epTitle: null,
|
epTitle: null,
|
||||||
prevBtn: null,
|
prevBtn: null,
|
||||||
nextBtn: null
|
nextBtn: null,
|
||||||
|
mpvBtn: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function init(animeId, initialSource, isLocal, animeData) {
|
function init(animeId, initialSource, isLocal, animeData) {
|
||||||
@@ -40,10 +43,8 @@ const AnimePlayer = (function() {
|
|||||||
_isLocal = isLocal;
|
_isLocal = isLocal;
|
||||||
_malId = animeData.idMal || null;
|
_malId = animeData.idMal || null;
|
||||||
|
|
||||||
// Guardar total de episodios
|
|
||||||
_totalEpisodes = animeData.episodes || 1000;
|
_totalEpisodes = animeData.episodes || 1000;
|
||||||
|
|
||||||
// Extraer título para RPC (Lógica traída de player.js)
|
|
||||||
if (animeData.title) {
|
if (animeData.title) {
|
||||||
_animeTitle = animeData.title.romaji || animeData.title.english || animeData.title.native || animeData.title || "Anime";
|
_animeTitle = animeData.title.romaji || animeData.title.english || animeData.title.native || animeData.title || "Anime";
|
||||||
}
|
}
|
||||||
@@ -51,13 +52,15 @@ const AnimePlayer = (function() {
|
|||||||
_skipIntervals = [];
|
_skipIntervals = [];
|
||||||
_localEntryId = null;
|
_localEntryId = null;
|
||||||
|
|
||||||
// --- REFERENCIAS DOM ---
|
|
||||||
els.wrapper = document.getElementById('hero-wrapper');
|
els.wrapper = document.getElementById('hero-wrapper');
|
||||||
els.playerWrapper = document.getElementById('player-wrapper');
|
els.playerWrapper = document.getElementById('player-wrapper');
|
||||||
els.video = document.getElementById('player');
|
els.video = document.getElementById('player');
|
||||||
els.loader = document.getElementById('player-loading');
|
els.loader = document.getElementById('player-loading');
|
||||||
els.loaderText = document.getElementById('player-loading-text');
|
els.loaderText = document.getElementById('player-loading-text');
|
||||||
|
|
||||||
|
els.mpvBtn = document.getElementById('mpv-btn');
|
||||||
|
if (els.mpvBtn) els.mpvBtn.addEventListener('click', openInMPV);
|
||||||
|
|
||||||
els.serverSelect = document.getElementById('server-select');
|
els.serverSelect = document.getElementById('server-select');
|
||||||
els.extSelect = document.getElementById('extension-select');
|
els.extSelect = document.getElementById('extension-select');
|
||||||
els.subDubToggle = document.getElementById('sd-toggle');
|
els.subDubToggle = document.getElementById('sd-toggle');
|
||||||
@@ -69,11 +72,9 @@ const AnimePlayer = (function() {
|
|||||||
const closeBtn = document.getElementById('close-player-btn');
|
const closeBtn = document.getElementById('close-player-btn');
|
||||||
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
||||||
|
|
||||||
// Configuración de navegación
|
|
||||||
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1));
|
if(els.prevBtn) els.prevBtn.addEventListener('click', () => playEpisode(_currentEpisode - 1));
|
||||||
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1));
|
if(els.nextBtn) els.nextBtn.addEventListener('click', () => playEpisode(_currentEpisode + 1));
|
||||||
|
|
||||||
// Botón Flotante (Skip)
|
|
||||||
if (!document.getElementById('skip-overlay-btn')) {
|
if (!document.getElementById('skip-overlay-btn')) {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.id = 'skip-overlay-btn';
|
btn.id = 'skip-overlay-btn';
|
||||||
@@ -85,7 +86,6 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
if(_skipBtn) _skipBtn.onclick = () => handleOverlayClick();
|
if(_skipBtn) _skipBtn.onclick = () => handleOverlayClick();
|
||||||
|
|
||||||
// Listeners Controles
|
|
||||||
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
||||||
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
||||||
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
||||||
@@ -93,7 +93,51 @@ const AnimePlayer = (function() {
|
|||||||
loadExtensionsList();
|
loadExtensionsList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- FUNCIÓN RPC (Integrada desde player.js) ---
|
async function openInMPV() {
|
||||||
|
if (!_rawVideoData) {
|
||||||
|
alert("No video loaded yet.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
alert("You need to be logged in.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const body = {
|
||||||
|
title: `${_animeTitle} - Episode ${_currentEpisode}`,
|
||||||
|
video: _rawVideoData,
|
||||||
|
subtitles: _currentSubtitles,
|
||||||
|
chapters: _skipIntervals,
|
||||||
|
animeId: _animeId,
|
||||||
|
episode: _currentEpisode,
|
||||||
|
entrySource: _entrySource,
|
||||||
|
token: localStorage.getItem('token')
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/watch/mpv', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
console.log("MPV Request Sent");
|
||||||
|
closePlayer();
|
||||||
|
} else {
|
||||||
|
console.error("MPV Request Failed");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("MPV Error:", e);
|
||||||
|
} finally {
|
||||||
|
if(els.mpvBtn) {
|
||||||
|
els.mpvBtn.innerHTML = originalContent;
|
||||||
|
els.mpvBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
function sendRPC({ startTimestamp, endTimestamp, paused = false } = {}) {
|
||||||
fetch("/api/rpc", {
|
fetch("/api/rpc", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -158,7 +202,6 @@ const AnimePlayer = (function() {
|
|||||||
const trailer = document.querySelector('#trailer-player iframe');
|
const trailer = document.querySelector('#trailer-player iframe');
|
||||||
if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
||||||
|
|
||||||
// Reset RPC state on new episode
|
|
||||||
_rpcActive = false;
|
_rpcActive = false;
|
||||||
|
|
||||||
if (els.extSelect.value === 'local') {
|
if (els.extSelect.value === 'local') {
|
||||||
@@ -185,7 +228,6 @@ const AnimePlayer = (function() {
|
|||||||
_skipIntervals = [];
|
_skipIntervals = [];
|
||||||
_rpcActive = false;
|
_rpcActive = false;
|
||||||
|
|
||||||
// Enviar señal de pausa o limpieza al cerrar
|
|
||||||
sendRPC({ paused: true });
|
sendRPC({ paused: true });
|
||||||
|
|
||||||
const newUrl = new URL(window.location);
|
const newUrl = new URL(window.location);
|
||||||
@@ -294,6 +336,9 @@ const AnimePlayer = (function() {
|
|||||||
_progressUpdated = false;
|
_progressUpdated = false;
|
||||||
setLoading("Fetching Stream...");
|
setLoading("Fetching Stream...");
|
||||||
|
|
||||||
|
_rawVideoData = null;
|
||||||
|
_currentSubtitles = [];
|
||||||
|
|
||||||
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
||||||
|
|
||||||
const currentExt = els.extSelect.value;
|
const currentExt = els.extSelect.value;
|
||||||
@@ -306,6 +351,13 @@ const AnimePlayer = (function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const localUrl = `/api/library/stream/anime/${localId}/${_currentEpisode}`;
|
const localUrl = `/api/library/stream/anime/${localId}/${_currentEpisode}`;
|
||||||
|
|
||||||
|
_rawVideoData = {
|
||||||
|
url: window.location.origin + localUrl,
|
||||||
|
headers: {}
|
||||||
|
};
|
||||||
|
_currentSubtitles = [];
|
||||||
|
|
||||||
initVideoPlayer(localUrl, 'mp4');
|
initVideoPlayer(localUrl, 'mp4');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
setLoading("Local Error: " + e.message);
|
setLoading("Local Error: " + e.message);
|
||||||
@@ -330,6 +382,11 @@ const AnimePlayer = (function() {
|
|||||||
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
const source = data.videoSources.find(s => s.type === 'm3u8') || data.videoSources[0];
|
||||||
const headers = data.headers || {};
|
const headers = data.headers || {};
|
||||||
|
|
||||||
|
_rawVideoData = {
|
||||||
|
url: source.url,
|
||||||
|
headers: headers
|
||||||
|
};
|
||||||
|
|
||||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
||||||
if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
||||||
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
||||||
@@ -340,6 +397,12 @@ const AnimePlayer = (function() {
|
|||||||
src: `/api/proxy?url=${encodeURIComponent(sub.url)}`
|
src: `/api/proxy?url=${encodeURIComponent(sub.url)}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
_currentSubtitles = (source.subtitles || []).map(sub => ({
|
||||||
|
label: sub.language,
|
||||||
|
srclang: sub.id,
|
||||||
|
src: sub.url
|
||||||
|
}));
|
||||||
|
|
||||||
initVideoPlayer(proxyUrl, source.type, subtitles);
|
initVideoPlayer(proxyUrl, source.type, subtitles);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setLoading("Stream Error: " + err.message);
|
setLoading("Stream Error: " + err.message);
|
||||||
@@ -350,17 +413,10 @@ const AnimePlayer = (function() {
|
|||||||
const video = els.video;
|
const video = els.video;
|
||||||
Array.from(video.querySelectorAll('track')).forEach(t => t.remove());
|
Array.from(video.querySelectorAll('track')).forEach(t => t.remove());
|
||||||
|
|
||||||
// Limpiar listeners de video antiguos para evitar duplicados en RPC
|
|
||||||
const newVideo = video.cloneNode(true);
|
const newVideo = video.cloneNode(true);
|
||||||
video.parentNode.replaceChild(newVideo, video);
|
video.parentNode.replaceChild(newVideo, video);
|
||||||
els.video = newVideo;
|
els.video = newVideo;
|
||||||
// Nota: Al clonar perdemos referencia en 'els', hay que reasignar
|
|
||||||
// Sin embargo, clonar rompe Plyr si no se tiene cuidado.
|
|
||||||
// Mejor estrategia: Remover listeners específicos si fuera posible,
|
|
||||||
// pero dado que son anónimos, la clonación es efectiva si reinicializamos todo.
|
|
||||||
// Como initPlyr se llama después, esto funciona.
|
|
||||||
|
|
||||||
// --- INYECCIÓN DE EVENTOS RPC ---
|
|
||||||
els.video.addEventListener("play", () => {
|
els.video.addEventListener("play", () => {
|
||||||
if (!els.video.duration) return;
|
if (!els.video.duration) return;
|
||||||
const elapsed = Math.floor(els.video.currentTime);
|
const elapsed = Math.floor(els.video.currentTime);
|
||||||
@@ -381,7 +437,6 @@ const AnimePlayer = (function() {
|
|||||||
const end = start + Math.floor(els.video.duration);
|
const end = start + Math.floor(els.video.duration);
|
||||||
sendRPC({ startTimestamp: start, endTimestamp: end });
|
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||||
});
|
});
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
if (Hls.isSupported() && (type === 'm3u8' || url.includes('.m3u8'))) {
|
if (Hls.isSupported() && (type === 'm3u8' || url.includes('.m3u8'))) {
|
||||||
hlsInstance = new Hls();
|
hlsInstance = new Hls();
|
||||||
@@ -430,7 +485,7 @@ const AnimePlayer = (function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initPlyr() {
|
function initPlyr() {
|
||||||
// Asegurarnos de usar el elemento video actualizado
|
|
||||||
if (plyrInstance) return;
|
if (plyrInstance) return;
|
||||||
|
|
||||||
plyrInstance = new Plyr(els.video, {
|
plyrInstance = new Plyr(els.video, {
|
||||||
|
|||||||
@@ -74,6 +74,12 @@
|
|||||||
|
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
|
<button id="mpv-btn" class="glass-btn-mpv" title="Open in MPV">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M5 3l14 9-14 9V3z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>MPV</span>
|
||||||
|
</button>
|
||||||
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
<div class="sd-toggle" id="sd-toggle" data-state="sub">
|
||||||
<div class="sd-bg"></div>
|
<div class="sd-bg"></div>
|
||||||
<div class="sd-option active" id="opt-sub">Sub</div>
|
<div class="sd-option active" id="opt-sub">Sub</div>
|
||||||
|
|||||||
@@ -504,4 +504,36 @@ body.stop-scrolling {
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
visibility: visible !important;
|
visibility: visible !important;
|
||||||
pointer-events: auto !important;
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-mpv {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
color: white;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
height: 36px; /* Para igualar la altura de los selects/toggles */
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-mpv:hover {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-mpv svg {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-btn-mpv:active {
|
||||||
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user