added open in mpv on anime for electron ver

This commit is contained in:
2025-12-31 15:11:45 +01:00
parent 9daa4f1ad8
commit 5e96a89a4b
4 changed files with 136 additions and 30 deletions

View File

@@ -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) {

View File

@@ -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, {

View File

@@ -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>

View File

@@ -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);
}