added rpc support for mpv and tracking
This commit is contained in:
@@ -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'
|
||||
) {
|
||||
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');
|
||||
|
||||
socket.write(JSON.stringify({
|
||||
command: ['set_property', 'force-media-title', mediaTitle]
|
||||
}) + '\n');
|
||||
|
||||
titleChanged = true;
|
||||
clearTimeout(safetyTimeout);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!socket.destroyed) socket.end();
|
||||
}, 500);
|
||||
sendRPC(false);
|
||||
titleChanged = true;
|
||||
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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user