added backend for open mpv in electron version
This commit is contained in:
@@ -2,6 +2,11 @@ import {FastifyReply, FastifyRequest} from 'fastify';
|
|||||||
import * as animeService from './anime.service';
|
import * as animeService from './anime.service';
|
||||||
import {getExtension} from '../../shared/extensions';
|
import {getExtension} from '../../shared/extensions';
|
||||||
import {Anime, AnimeRequest, SearchRequest, WatchStreamRequest} from '../types';
|
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) {
|
export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
|
||||||
try {
|
try {
|
||||||
@@ -105,3 +110,157 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
|
|||||||
return { error: error.message };
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ async function animeRoutes(fastify: FastifyInstance) {
|
|||||||
fastify.get('/search', controller.search);
|
fastify.get('/search', controller.search);
|
||||||
fastify.get('/search/:extension', controller.searchInExtension);
|
fastify.get('/search/:extension', controller.searchInExtension);
|
||||||
fastify.get('/watch/stream', controller.getWatchStream);
|
fastify.get('/watch/stream', controller.getWatchStream);
|
||||||
|
fastify.post('/watch/mpv', controller.openInMPV);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default animeRoutes;
|
export default animeRoutes;
|
||||||
Reference in New Issue
Block a user