added rpc support for mpv and tracking

This commit is contained in:
2025-12-31 14:47:37 +01:00
parent 47433733d0
commit 9daa4f1ad8
2 changed files with 98 additions and 62 deletions

View File

@@ -1,5 +1,7 @@
import {FastifyReply, FastifyRequest} from 'fastify';
import * as animeService from './anime.service';
import { setActivity } from '../rpc/rp.service';
import { upsertListEntry } from '../list/list.service';
import {getExtension} from '../../shared/extensions';
import {Anime, AnimeRequest, SearchRequest, WatchStreamRequest} from '../types';
import {spawn} from "node:child_process";
@@ -7,6 +9,7 @@ import net from 'net';
import fs from 'fs';
import os from 'os';
import path from 'path';
import jwt from "jsonwebtoken";
export async function getAnime(req: AnimeRequest, reply: FastifyReply) {
try {
@@ -113,10 +116,13 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl
export async function openInMPV(req: any, reply: any) {
try {
const { title, video, subtitles = [], chapters = [] } = req.body;
const { title, video, subtitles = [], chapters = [], animeId, episode, entrySource, token } = req.body;
if (!video?.url) return { error: 'Missing video url' };
const proxyBase = 'http://localhost:54322/api/proxy';
const PORT = 54322;
const proxyBase = `http://localhost:${PORT}/api/proxy`;
const mediaTitle = title || 'Anime';
const proxyVideo =
@@ -136,9 +142,7 @@ 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);
const lines = [';FFMETADATA1'];
for (let i = 0; i < chapters.length; i++) {
@@ -147,40 +151,24 @@ export async function openInMPV(req: any, reply: any) {
const end = Math.floor(c.interval.endTime * 1000);
lines.push(
`[CHAPTER]`,
`TIMEBASE=1/1000`,
`START=${start}`,
`END=${end}`,
`title=${c.skipType.toUpperCase()}`
`[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`
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${end}`, `END=${nextStart}`, `title=Episode`
);
}
} else {
lines.push(
`[CHAPTER]`,
`TIMEBASE=1/1000`,
`START=${end}`,
`title=Episode`
`[CHAPTER]`, `TIMEBASE=1/1000`, `START=${end}`, `title=Episode`
);
}
}
const chaptersFile = path.join(
os.tmpdir(),
`mpv-chapters-${Date.now()}.txt`
);
const chaptersFile = path.join(os.tmpdir(), `mpv-chapters-${Date.now()}.txt`);
fs.writeFileSync(chaptersFile, lines.join('\n'));
chaptersArg = [`--chapters-file=${chaptersFile}`];
}
@@ -198,21 +186,57 @@ export async function openInMPV(req: any, reply: any) {
`--input-ipc-server=${pipe}`,
...chaptersArg
],
{
stdio: 'ignore',
windowsHide: true,
detached: true
}
{ stdio: 'ignore', windowsHide: true, detached: true }
).unref();
await new Promise(r => setTimeout(r, 300));
await new Promise(r => setTimeout(r, 400));
const socket = net.connect(pipe);
let duration = 0;
let currentTime = 0;
let isPaused = false;
let titleChanged = false;
let progressUpdated = false;
const safetyTimeout = setTimeout(() => {
if (!socket.destroyed) socket.end();
}, 15000);
const sendRPC = (paused: boolean) => {
let startTimestamp = undefined;
let endTimestamp = undefined;
if (!paused && duration > 0) {
const now = Math.floor(Date.now() / 1000);
const elapsed = Math.floor(currentTime);
startTimestamp = now - elapsed;
endTimestamp = startTimestamp + Math.floor(duration);
}
setActivity({
details: mediaTitle,
state: `Episode ${episode}`,
mode: "watching",
paused: paused,
startTimestamp: startTimestamp,
endTimestamp: endTimestamp
})
};
const updateProgress = async () => {
if (!token || progressUpdated) return;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
const userId = decoded.id;
await upsertListEntry({
user_id: userId,
entry_id: animeId,
source: entrySource,
entry_type: "ANIME",
status: "CURRENT",
progress: episode
});
progressUpdated = true;
} catch (e) { console.error("[MPV] Progress update failed", e); }
};
const loadingTimeout = setTimeout(() => { if (!socket.destroyed) socket.end(); }, 15000);
socket.on('data', (data) => {
const chunks = data.toString().split('\n');
@@ -221,42 +245,56 @@ export async function openInMPV(req: any, reply: any) {
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');
if (json.event === 'property-change' && json.name === 'duration' && typeof json.data === 'number') {
duration = json.data;
if (!titleChanged) {
socket.write(JSON.stringify({ command: ['set_property', 'force-media-title', mediaTitle] }) + '\n');
sendRPC(false);
titleChanged = true;
clearTimeout(safetyTimeout);
setTimeout(() => {
if (!socket.destroyed) socket.end();
}, 500);
clearTimeout(loadingTimeout);
}
} catch (err) {
}
if (json.event === 'property-change' && json.name === 'time-pos' && typeof json.data === 'number') {
currentTime = json.data;
if (duration > 0 && !progressUpdated && (currentTime / duration) >= 0.8) {
updateProgress();
}
}
if (json.event === 'property-change' && json.name === 'pause') {
isPaused = json.data === true;
sendRPC(isPaused);
}
if (json.event === 'seek') {
setTimeout(() => sendRPC(isPaused), 100);
}
if (json.event === 'end-file' || json.event === 'shutdown') {
sendRPC(true);
if (!socket.destroyed) socket.destroy();
}
} catch (err) {}
}
});
socket.write(JSON.stringify({
command: ['observe_property', 1, 'duration']
}) + '\n');
const commands = [
{ command: ['observe_property', 1, 'duration'] },
{ command: ['observe_property', 2, 'time-pos'] },
{ command: ['observe_property', 3, 'pause'] },
{ command: ['loadfile', proxyVideo, 'replace'] }
];
socket.write(JSON.stringify({
command: ['loadfile', proxyVideo, 'replace']
}) + '\n');
commands.forEach(cmd => socket.write(JSON.stringify(cmd) + '\n'));
for (const sub of proxySubs) {
socket.write(JSON.stringify({
command: ['sub-add', sub, 'auto']
}) + '\n');
socket.write(JSON.stringify({ command: ['sub-add', sub, 'auto'] }) + '\n');
}
return { success: true };

View File

@@ -4,8 +4,6 @@ let rpcClient: Client | null = null;
let reconnectTimer: NodeJS.Timeout | null = null;
let connected = false;
type RPCMode = "watching" | "reading" | string;
interface RPCData {
details?: string;
state?: string;