From d0cc461c1c6d26e833600207998bc9824082e051 Mon Sep 17 00:00:00 2001 From: lenafx Date: Thu, 4 Dec 2025 03:13:13 +0100 Subject: [PATCH] added rpc --- package-lock.json | 56 +++++++++++++++- package.json | 6 +- server.js | 8 ++- src/api/rpc/rp.service.ts | 120 ++++++++++++++++++++++++++++++++++ src/api/rpc/rpc.controller.ts | 27 ++++++++ src/api/rpc/rpc.routes.ts | 8 +++ src/scripts/anime/player.js | 10 +++ src/scripts/books/reader.js | 25 ++++++- src/scripts/rpc-inapp.js | 9 +++ views/anime/anime.html | 1 + views/anime/index.html | 1 + views/books/book.html | 1 + views/books/books.html | 1 + views/gallery/gallery.html | 1 + views/gallery/image.html | 1 + views/schedule.html | 1 + 16 files changed, 268 insertions(+), 8 deletions(-) create mode 100644 src/api/rpc/rp.service.ts create mode 100644 src/api/rpc/rpc.controller.ts create mode 100644 src/api/rpc/rpc.routes.ts create mode 100644 src/scripts/rpc-inapp.js diff --git a/package-lock.json b/package-lock.json index f7f27dc..227c18d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "ISC", "dependencies": { "@fastify/static": "^8.3.0", + "@ryuziii/discord-rpc": "^1.0.1-rc.1", "bindings": "^1.5.0", + "dotenv": "^17.2.3", "fastify": "^5.6.2", "node-addon-api": "^8.5.0", "playwright-chromium": "1.57.0", @@ -356,6 +358,16 @@ "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", "license": "MIT" }, + "node_modules/@ryuziii/discord-rpc": { + "version": "1.0.1-rc.1", + "resolved": "https://registry.npmjs.org/@ryuziii/discord-rpc/-/discord-rpc-1.0.1-rc.1.tgz", + "integrity": "sha512-q9YgU8Rj9To1LWzo4u8cXOHUorEkB5KZ5cdW80KoYtAUx+nQy7wYCEFiNh8kcmqFqQ8m3Fsx1IXx3UpVymkaSw==", + "license": "ISC", + "dependencies": { + "@types/ws": "^8.18.1", + "ws": "^8.18.3" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -398,12 +410,20 @@ "version": "24.10.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.16.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/abbrev": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz", @@ -917,6 +937,18 @@ "node": ">=0.3.1" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -3300,7 +3332,6 @@ "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", - "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -3509,6 +3540,27 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/package.json b/package.json index fb6cc19..3827ef3 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,13 @@ "type": "commonjs", "dependencies": { "@fastify/static": "^8.3.0", + "@ryuziii/discord-rpc": "^1.0.1-rc.1", "bindings": "^1.5.0", + "dotenv": "^17.2.3", "fastify": "^5.6.2", "node-addon-api": "^8.5.0", - "sqlite3": "^5.1.7", - "playwright-chromium": "1.57.0" + "playwright-chromium": "1.57.0", + "sqlite3": "^5.1.7" }, "devDependencies": { "@types/node": "^24.0.0", diff --git a/server.js b/server.js index 499b4d8..d7ce417 100644 --- a/server.js +++ b/server.js @@ -3,9 +3,11 @@ const path = require('path'); const { spawn } = require('child_process'); const fs = require('fs'); import { initHeadless } from "./src/shared/headless"; - const { initDatabase } = require('./src/shared/database'); const { loadExtensions } = require('./src/shared/extensions'); +const { init } = require('./src/api/rpc/rpc.controller'); +import * as dotenv from 'dotenv'; +dotenv.config(); const viewsRoutes = require('./src/views/views.routes'); const animeRoutes = require('./src/api/anime/anime.routes'); @@ -13,6 +15,7 @@ const booksRoutes = require('./src/api/books/books.routes'); const proxyRoutes = require('./src/api/proxy/proxy.routes'); const extensionsRoutes = require('./src/api/extensions/extensions.routes'); const galleryRoutes = require('./src/api/gallery/gallery.routes'); +const rpcRoutes = require('./src/api/rpc/rpc.routes'); fastify.register(require('@fastify/static'), { root: path.join(__dirname, 'public'), @@ -38,6 +41,7 @@ fastify.register(booksRoutes, { prefix: '/api' }); fastify.register(proxyRoutes, { prefix: '/api' }); fastify.register(extensionsRoutes, { prefix: '/api' }); fastify.register(galleryRoutes, { prefix: '/api' }); +fastify.register(rpcRoutes, { prefix: '/api' }); function startCppScraper() { const exePath = path.join( @@ -79,6 +83,8 @@ const start = async () => { initDatabase("anilist"); initDatabase("favorites"); initDatabase("cache"); + init() + await loadExtensions(); await fastify.listen({ port: 3000, host: '0.0.0.0' }); diff --git a/src/api/rpc/rp.service.ts b/src/api/rpc/rp.service.ts new file mode 100644 index 0000000..dd3e98b --- /dev/null +++ b/src/api/rpc/rp.service.ts @@ -0,0 +1,120 @@ +// @ts-ignore +import { DiscordRPCClient } from "@ryuziii/discord-rpc"; + +let rpcClient: DiscordRPCClient | null = null; +let reconnectTimer: NodeJS.Timeout | null = null; +let connected: boolean = false; + +type RPCMode = "watching" | "reading" | string; + +interface RPCData { + details?: string; + state?: string; + mode?: RPCMode; + version?: string; +} + +function attemptReconnect(clientId: string) { + connected = false; + + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + console.log('Discord RPC: Trying to reconnect...'); + + reconnectTimer = setTimeout(() => { + initRPC(clientId); + }, 10000); +} + +export function initRPC(clientId: string) { + + if (rpcClient) { + try { rpcClient.destroy(); } catch (e) {} + rpcClient = null; + } + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + console.log(`Discord RPC: Starting with id ...${clientId.slice(-4)}`); + + try { + + rpcClient = new DiscordRPCClient({ + clientId: clientId, + transport: 'ipc' + }); + } catch (err) { + console.error('Discord RPC:', err); + return; + } + + rpcClient.on("ready", () => { + connected = true; + const user = rpcClient?.user ? rpcClient.user.username : 'User'; + console.log(`Discord RPC: Authenticated for: ${user}`); + + setTimeout(() => { + setActivity({ details: "Browsing", state: "In App", mode: "idle" }); + }, 1000); + }); + + rpcClient.on('disconnected', () => { + console.log('Discord RPC: Desconexión detectada.'); + attemptReconnect(clientId); + }); + + rpcClient.on('error', (err: { message: any; }) => { + console.error('[Discord RPC] Error:', err.message); + + if (connected) { + attemptReconnect(clientId); + } + }); + + try { + rpcClient.connect().catch((err: { message: any; }) => { + console.error('Discord RPC: Error al conectar', err.message); + + attemptReconnect(clientId); + }); + } catch (err) { + console.error('Discord RPC: Error al iniciar la conexión', err); + attemptReconnect(clientId); + } +} + +export function setActivity(data: RPCData = {}) { + if (!rpcClient || !connected) return; + + let type; + let state = data.state; + let details = data.details; + + if (data.mode === "watching") { + type = 3 + } else if (data.mode === "reading") { + type = 0 + } else { + type = 0 + } + + + try { + rpcClient.setActivity({ + details: details, + state: state, + type: type, + startTimestamp: new Date(), + largeImageKey: "bigpicture", + largeImageText: "v2.0.0", + instance: false + }); + } catch (error) { + console.error("Discord RPC: Failed to set activity", error); + } +} \ No newline at end of file diff --git a/src/api/rpc/rpc.controller.ts b/src/api/rpc/rpc.controller.ts new file mode 100644 index 0000000..42c0a28 --- /dev/null +++ b/src/api/rpc/rpc.controller.ts @@ -0,0 +1,27 @@ +import { FastifyRequest, FastifyReply } from "fastify"; +import { setActivity, initRPC } from "./rp.service"; + +let initialized = false; + +export function init() { + if (!initialized) { + initRPC(process.env.DISCORD_CLIENT_ID!); + initialized = true; + } +} + +export async function setRPC(request: FastifyRequest, reply: FastifyReply) { + const { details, state, mode } = request.body as { + details?: string; + state?: string; + mode?: "watching" | "reading" | string; + }; + + setActivity({ + details, + state, + mode + }); + + return reply.send({ ok: true }); +} \ No newline at end of file diff --git a/src/api/rpc/rpc.routes.ts b/src/api/rpc/rpc.routes.ts new file mode 100644 index 0000000..23382e1 --- /dev/null +++ b/src/api/rpc/rpc.routes.ts @@ -0,0 +1,8 @@ +import { FastifyInstance } from "fastify"; +import * as controller from "./rpc.controller"; + +async function rpcRoutes(fastify: FastifyInstance) { + fastify.post("/rpc", controller.setRPC); +} + +export default rpcRoutes; \ No newline at end of file diff --git a/src/scripts/anime/player.js b/src/scripts/anime/player.js index 7b79b9e..0d0d0c2 100644 --- a/src/scripts/anime/player.js +++ b/src/scripts/anime/player.js @@ -66,6 +66,16 @@ async function loadMetadata() { document.getElementById('anime-title-details2').innerText = title; document.title = `Watching ${title} - Ep ${currentEpisode}`; + fetch("/api/rpc", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + details: title, + state: `Episode ${currentEpisode}`, + mode: "watching" + }) + }); + const tempDiv = document.createElement('div'); tempDiv.innerHTML = description; document.getElementById('detail-description').innerText = tempDiv.textContent || tempDiv.innerText || 'No description available.'; diff --git a/src/scripts/books/reader.js b/src/scripts/books/reader.js index 2e2bcca..2c344e0 100644 --- a/src/scripts/books/reader.js +++ b/src/scripts/books/reader.js @@ -10,8 +10,6 @@ const nextBtn = document.getElementById('next-chapter'); const lnSettings = document.getElementById('ln-settings'); const mangaSettings = document.getElementById('manga-settings'); -const hasQuery = window.location.search.length > 0; - const config = { ln: { fontSize: 18, @@ -145,6 +143,18 @@ async function loadChapter() { document.title = `Chapter ${chapter}`; } + const res2 = await fetch(`/api/book/${bookId}?source=${source}`); + const data2 = await res2.json(); + + fetch("/api/rpc", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + details: data2.title.romaji ?? data2.title, + state: `Chapter ${data.title}`, + mode: "reading" + }) + }); if (data.error) { reader.innerHTML = `
@@ -476,7 +486,16 @@ nextBtn.addEventListener('click', () => { function updateURL(newChapter) { chapter = newChapter; - const newUrl = `/read/${provider}/${chapter}/${bookId}`; + const urlParams = new URLSearchParams(window.location.search); + let source = urlParams.get('source'); + + let src; + if (source === 'anilist') { + src= "?source=anilist" + } else { + src= `?source=${source}` + } + const newUrl = `/read/${provider}/${chapter}/${bookId}${src}`; window.history.pushState({}, '', newUrl); } diff --git a/src/scripts/rpc-inapp.js b/src/scripts/rpc-inapp.js new file mode 100644 index 0000000..943d2ba --- /dev/null +++ b/src/scripts/rpc-inapp.js @@ -0,0 +1,9 @@ +fetch("/api/rpc", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + details: "Browsing", + state: `In App`, + mode: "idle" + }) +}); \ No newline at end of file diff --git a/views/anime/anime.html b/views/anime/anime.html index f2806fe..88e41bd 100644 --- a/views/anime/anime.html +++ b/views/anime/anime.html @@ -142,6 +142,7 @@
+ diff --git a/views/anime/index.html b/views/anime/index.html index 71fa74f..6115d31 100644 --- a/views/anime/index.html +++ b/views/anime/index.html @@ -119,5 +119,6 @@ + \ No newline at end of file diff --git a/views/books/book.html b/views/books/book.html index d5d3e05..841cdb9 100644 --- a/views/books/book.html +++ b/views/books/book.html @@ -125,6 +125,7 @@ + \ No newline at end of file diff --git a/views/books/books.html b/views/books/books.html index a3bfedf..304beae 100644 --- a/views/books/books.html +++ b/views/books/books.html @@ -95,5 +95,6 @@ + \ No newline at end of file diff --git a/views/gallery/gallery.html b/views/gallery/gallery.html index c22c1e4..1f15d85 100644 --- a/views/gallery/gallery.html +++ b/views/gallery/gallery.html @@ -82,5 +82,6 @@ + \ No newline at end of file diff --git a/views/gallery/image.html b/views/gallery/image.html index 33b3533..7196793 100644 --- a/views/gallery/image.html +++ b/views/gallery/image.html @@ -67,5 +67,6 @@ + \ No newline at end of file diff --git a/views/schedule.html b/views/schedule.html index 8fc33f0..85f0414 100644 --- a/views/schedule.html +++ b/views/schedule.html @@ -96,5 +96,6 @@ + \ No newline at end of file