From 47433733d0ad334e5f074d032bf07558b10bd3d2 Mon Sep 17 00:00:00 2001 From: lenafx Date: Wed, 31 Dec 2025 14:31:20 +0100 Subject: [PATCH] added backend for open mpv in electron version --- desktop/src/api/anime/anime.controller.ts | 159 ++++++++++++++++++++++ desktop/src/api/anime/anime.routes.ts | 1 + 2 files changed, 160 insertions(+) diff --git a/desktop/src/api/anime/anime.controller.ts b/desktop/src/api/anime/anime.controller.ts index b6bd693..ef6ba03 100644 --- a/desktop/src/api/anime/anime.controller.ts +++ b/desktop/src/api/anime/anime.controller.ts @@ -2,6 +2,11 @@ import {FastifyReply, FastifyRequest} from 'fastify'; import * as animeService from './anime.service'; import {getExtension} from '../../shared/extensions'; import {Anime, AnimeRequest, SearchRequest, WatchStreamRequest} from '../types'; +import {spawn} from "node:child_process"; +import net from 'net'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; export async function getAnime(req: AnimeRequest, reply: FastifyReply) { try { @@ -104,4 +109,158 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl const error = err as Error; return { error: error.message }; } +} + +export async function openInMPV(req: any, reply: any) { + try { + const { title, video, subtitles = [], chapters = [] } = req.body; + if (!video?.url) return { error: 'Missing video url' }; + + const proxyBase = 'http://localhost:54322/api/proxy'; + const mediaTitle = title || 'Anime'; + + const proxyVideo = + `${proxyBase}?url=${encodeURIComponent(video.url)}` + + `&referer=${encodeURIComponent(video.headers?.Referer ?? '')}` + + `&origin=${encodeURIComponent(video.headers?.Origin ?? '')}` + + `&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}`; + + const proxySubs = subtitles.map((s: any) => + `${proxyBase}?url=${encodeURIComponent(s.url)}` + + `&referer=${encodeURIComponent(video.headers?.Referer ?? '')}` + + `&origin=${encodeURIComponent(video.headers?.Origin ?? '')}` + + `&userAgent=${encodeURIComponent(video.headers?.['User-Agent'] ?? '')}` + ); + + const pipe = `\\\\.\\pipe\\mpv-${Date.now()}`; + + let chaptersArg: string[] = []; + if (chapters.length) { + + chapters.sort((a: any, b: any) => a.interval.startTime - b.interval.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); + + lines.push( + `[CHAPTER]`, + `TIMEBASE=1/1000`, + `START=${start}`, + `END=${end}`, + `title=${c.skipType.toUpperCase()}` + ); + + if (i < chapters.length - 1) { + const nextStart = Math.floor(chapters[i + 1].interval.startTime * 1000); + if (nextStart - end > 1000) { + lines.push( + `[CHAPTER]`, + `TIMEBASE=1/1000`, + `START=${end}`, + `END=${nextStart}`, + `title=Episode` + ); + } + } else { + + lines.push( + `[CHAPTER]`, + `TIMEBASE=1/1000`, + `START=${end}`, + `title=Episode` + ); + } + } + + const chaptersFile = path.join( + os.tmpdir(), + `mpv-chapters-${Date.now()}.txt` + ); + + fs.writeFileSync(chaptersFile, lines.join('\n')); + chaptersArg = [`--chapters-file=${chaptersFile}`]; + } + + spawn( + 'D:\\mpv\\mpv.exe', + [ + '--force-window=yes', + '--idle=yes', + '--keep-open=yes', + '--no-terminal', + '--hwdec=auto', + `--force-media-title=Loading...`, + + `--input-ipc-server=${pipe}`, + ...chaptersArg + ], + { + stdio: 'ignore', + windowsHide: true, + detached: true + } + ).unref(); + + await new Promise(r => setTimeout(r, 300)); + const socket = net.connect(pipe); + + let titleChanged = false; + + const safetyTimeout = setTimeout(() => { + if (!socket.destroyed) socket.end(); + }, 15000); + + socket.on('data', (data) => { + const chunks = data.toString().split('\n'); + for (const chunk of chunks) { + if (!chunk.trim()) continue; + try { + const json = JSON.parse(chunk); + + if ( + !titleChanged && + json.event === 'property-change' && + json.name === 'duration' && + typeof json.data === 'number' + ) { + + socket.write(JSON.stringify({ + command: ['set_property', 'force-media-title', mediaTitle] + }) + '\n'); + + titleChanged = true; + clearTimeout(safetyTimeout); + + setTimeout(() => { + if (!socket.destroyed) socket.end(); + }, 500); + } + } catch (err) { + + } + } + }); + + socket.write(JSON.stringify({ + command: ['observe_property', 1, 'duration'] + }) + '\n'); + + socket.write(JSON.stringify({ + command: ['loadfile', proxyVideo, 'replace'] + }) + '\n'); + + for (const sub of proxySubs) { + socket.write(JSON.stringify({ + command: ['sub-add', sub, 'auto'] + }) + '\n'); + } + + return { success: true }; + } catch (e) { + return { error: (e as Error).message }; + } } \ No newline at end of file diff --git a/desktop/src/api/anime/anime.routes.ts b/desktop/src/api/anime/anime.routes.ts index f23976d..38dc810 100644 --- a/desktop/src/api/anime/anime.routes.ts +++ b/desktop/src/api/anime/anime.routes.ts @@ -9,6 +9,7 @@ async function animeRoutes(fastify: FastifyInstance) { fastify.get('/search', controller.search); fastify.get('/search/:extension', controller.searchInExtension); fastify.get('/watch/stream', controller.getWatchStream); + fastify.post('/watch/mpv', controller.openInMPV); } export default animeRoutes; \ No newline at end of file