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) {
|
||||
try {
|
||||
|
||||
const { title, video, subtitles = [], chapters = [], animeId, episode, entrySource, token } = req.body;
|
||||
|
||||
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'] ?? '')}`;
|
||||
|
||||
const proxySubs = subtitles.map((s: any) =>
|
||||
`${proxyBase}?url=${encodeURIComponent(s.url)}` +
|
||||
`${proxyBase}?url=${encodeURIComponent(s.src)}` +
|
||||
`&referer=${encodeURIComponent(video.headers?.Referer ?? '')}` +
|
||||
`&origin=${encodeURIComponent(video.headers?.Origin ?? '')}` +
|
||||
`&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`
|
||||
@@ -142,20 +141,26 @@ export async function openInMPV(req: any, reply: any) {
|
||||
|
||||
let chaptersArg: string[] = [];
|
||||
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'];
|
||||
|
||||
for (let i = 0; i < chapters.length; 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(
|
||||
`[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) {
|
||||
const nextStart = Math.floor(chapters[i + 1].interval.startTime * 1000);
|
||||
const nextStart = Math.floor(chapters[i + 1].startTime * 1000);
|
||||
|
||||
if (nextStart - end > 1000) {
|
||||
lines.push(
|
||||
`[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'));
|
||||
|
||||
for (const sub of proxySubs) {
|
||||
socket.write(JSON.stringify({ command: ['sub-add', sub, 'auto'] }) + '\n');
|
||||
}
|
||||
subtitles.forEach((s: any, i: number) => {
|
||||
socket.write(JSON.stringify({
|
||||
command: [
|
||||
'sub-add',
|
||||
proxySubs[i],
|
||||
'auto',
|
||||
s.label || 'Subtitle',
|
||||
s.srclang || ''
|
||||
]
|
||||
}) + '\n');
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (e) {
|
||||
|
||||
@@ -10,10 +10,12 @@ const AnimePlayer = (function() {
|
||||
let _skipIntervals = [];
|
||||
let _progressUpdated = false;
|
||||
|
||||
// Variables nuevas para RPC
|
||||
let _animeTitle = "Anime";
|
||||
let _rpcActive = false;
|
||||
|
||||
let _rawVideoData = null;
|
||||
let _currentSubtitles = [];
|
||||
|
||||
let _localEntryId = null;
|
||||
let _totalEpisodes = 0;
|
||||
|
||||
@@ -31,7 +33,8 @@ const AnimePlayer = (function() {
|
||||
subDubToggle: null,
|
||||
epTitle: null,
|
||||
prevBtn: null,
|
||||
nextBtn: null
|
||||
nextBtn: null,
|
||||
mpvBtn: null
|
||||
};
|
||||
|
||||
function init(animeId, initialSource, isLocal, animeData) {
|
||||
@@ -40,10 +43,8 @@ const AnimePlayer = (function() {
|
||||
_isLocal = isLocal;
|
||||
_malId = animeData.idMal || null;
|
||||
|
||||
// Guardar total de episodios
|
||||
_totalEpisodes = animeData.episodes || 1000;
|
||||
|
||||
// Extraer título para RPC (Lógica traída de player.js)
|
||||
if (animeData.title) {
|
||||
_animeTitle = animeData.title.romaji || animeData.title.english || animeData.title.native || animeData.title || "Anime";
|
||||
}
|
||||
@@ -51,13 +52,15 @@ const AnimePlayer = (function() {
|
||||
_skipIntervals = [];
|
||||
_localEntryId = null;
|
||||
|
||||
// --- REFERENCIAS DOM ---
|
||||
els.wrapper = document.getElementById('hero-wrapper');
|
||||
els.playerWrapper = document.getElementById('player-wrapper');
|
||||
els.video = document.getElementById('player');
|
||||
els.loader = document.getElementById('player-loading');
|
||||
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.extSelect = document.getElementById('extension-select');
|
||||
els.subDubToggle = document.getElementById('sd-toggle');
|
||||
@@ -69,11 +72,9 @@ const AnimePlayer = (function() {
|
||||
const closeBtn = document.getElementById('close-player-btn');
|
||||
if(closeBtn) closeBtn.addEventListener('click', closePlayer);
|
||||
|
||||
// Configuración de navegación
|
||||
if(els.prevBtn) els.prevBtn.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')) {
|
||||
const btn = document.createElement('button');
|
||||
btn.id = 'skip-overlay-btn';
|
||||
@@ -85,7 +86,6 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
if(_skipBtn) _skipBtn.onclick = () => handleOverlayClick();
|
||||
|
||||
// Listeners Controles
|
||||
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
|
||||
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
|
||||
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
|
||||
@@ -93,7 +93,51 @@ const AnimePlayer = (function() {
|
||||
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 } = {}) {
|
||||
fetch("/api/rpc", {
|
||||
method: "POST",
|
||||
@@ -158,7 +202,6 @@ const AnimePlayer = (function() {
|
||||
const trailer = document.querySelector('#trailer-player iframe');
|
||||
if(trailer) trailer.contentWindow.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*');
|
||||
|
||||
// Reset RPC state on new episode
|
||||
_rpcActive = false;
|
||||
|
||||
if (els.extSelect.value === 'local') {
|
||||
@@ -185,7 +228,6 @@ const AnimePlayer = (function() {
|
||||
_skipIntervals = [];
|
||||
_rpcActive = false;
|
||||
|
||||
// Enviar señal de pausa o limpieza al cerrar
|
||||
sendRPC({ paused: true });
|
||||
|
||||
const newUrl = new URL(window.location);
|
||||
@@ -294,6 +336,9 @@ const AnimePlayer = (function() {
|
||||
_progressUpdated = false;
|
||||
setLoading("Fetching Stream...");
|
||||
|
||||
_rawVideoData = null;
|
||||
_currentSubtitles = [];
|
||||
|
||||
if (hlsInstance) { hlsInstance.destroy(); hlsInstance = null; }
|
||||
|
||||
const currentExt = els.extSelect.value;
|
||||
@@ -306,6 +351,13 @@ const AnimePlayer = (function() {
|
||||
return;
|
||||
}
|
||||
const localUrl = `/api/library/stream/anime/${localId}/${_currentEpisode}`;
|
||||
|
||||
_rawVideoData = {
|
||||
url: window.location.origin + localUrl,
|
||||
headers: {}
|
||||
};
|
||||
_currentSubtitles = [];
|
||||
|
||||
initVideoPlayer(localUrl, 'mp4');
|
||||
} catch(e) {
|
||||
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 headers = data.headers || {};
|
||||
|
||||
_rawVideoData = {
|
||||
url: source.url,
|
||||
headers: headers
|
||||
};
|
||||
|
||||
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
|
||||
if (headers['Referer'] && headers['Referer'] !== "null") proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
|
||||
if (headers['User-Agent']) proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
|
||||
@@ -340,6 +397,12 @@ const AnimePlayer = (function() {
|
||||
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);
|
||||
} catch (err) {
|
||||
setLoading("Stream Error: " + err.message);
|
||||
@@ -350,17 +413,10 @@ const AnimePlayer = (function() {
|
||||
const video = els.video;
|
||||
Array.from(video.querySelectorAll('track')).forEach(t => t.remove());
|
||||
|
||||
// Limpiar listeners de video antiguos para evitar duplicados en RPC
|
||||
const newVideo = video.cloneNode(true);
|
||||
video.parentNode.replaceChild(newVideo, video);
|
||||
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", () => {
|
||||
if (!els.video.duration) return;
|
||||
const elapsed = Math.floor(els.video.currentTime);
|
||||
@@ -381,7 +437,6 @@ const AnimePlayer = (function() {
|
||||
const end = start + Math.floor(els.video.duration);
|
||||
sendRPC({ startTimestamp: start, endTimestamp: end });
|
||||
});
|
||||
// -------------------------------
|
||||
|
||||
if (Hls.isSupported() && (type === 'm3u8' || url.includes('.m3u8'))) {
|
||||
hlsInstance = new Hls();
|
||||
@@ -430,7 +485,7 @@ const AnimePlayer = (function() {
|
||||
}
|
||||
|
||||
function initPlyr() {
|
||||
// Asegurarnos de usar el elemento video actualizado
|
||||
|
||||
if (plyrInstance) return;
|
||||
|
||||
plyrInstance = new Plyr(els.video, {
|
||||
|
||||
@@ -74,6 +74,12 @@
|
||||
|
||||
<div class="header-right">
|
||||
<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-bg"></div>
|
||||
<div class="sd-option active" id="opt-sub">Sub</div>
|
||||
|
||||
@@ -504,4 +504,36 @@ body.stop-scrolling {
|
||||
opacity: 1 !important;
|
||||
visibility: visible !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