mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract texthooker and subtitle websocket services
This commit is contained in:
57
src/core/services/subtitle-ws-service.ts
Normal file
57
src/core/services/subtitle-ws-service.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as path from "path";
|
||||||
|
import WebSocket from "ws";
|
||||||
|
|
||||||
|
export function hasMpvWebsocketPlugin(): boolean {
|
||||||
|
const mpvWebsocketPath = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
".config",
|
||||||
|
"mpv",
|
||||||
|
"mpv_websocket",
|
||||||
|
);
|
||||||
|
return fs.existsSync(mpvWebsocketPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SubtitleWebSocketService {
|
||||||
|
private server: WebSocket.Server | null = null;
|
||||||
|
|
||||||
|
public isRunning(): boolean {
|
||||||
|
return this.server !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(port: number, getCurrentSubtitleText: () => string): void {
|
||||||
|
this.server = new WebSocket.Server({ port, host: "127.0.0.1" });
|
||||||
|
|
||||||
|
this.server.on("connection", (ws: WebSocket) => {
|
||||||
|
console.log("WebSocket client connected");
|
||||||
|
const currentText = getCurrentSubtitleText();
|
||||||
|
if (currentText) {
|
||||||
|
ws.send(JSON.stringify({ sentence: currentText }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.on("error", (err: Error) => {
|
||||||
|
console.error("WebSocket server error:", err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public broadcast(text: string): void {
|
||||||
|
if (!this.server) return;
|
||||||
|
const message = JSON.stringify({ sentence: text });
|
||||||
|
for (const client of this.server.clients) {
|
||||||
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
|
client.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/core/services/texthooker-service.ts
Normal file
82
src/core/services/texthooker-service.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import * as http from "http";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export class TexthookerService {
|
||||||
|
private server: http.Server | null = null;
|
||||||
|
|
||||||
|
public isRunning(): boolean {
|
||||||
|
return this.server !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public start(port: number): http.Server | null {
|
||||||
|
const texthookerPath = this.getTexthookerPath();
|
||||||
|
if (!texthookerPath) {
|
||||||
|
console.error("texthooker-ui not found");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.server = http.createServer((req, res) => {
|
||||||
|
const urlPath = (req.url || "/").split("?")[0];
|
||||||
|
const filePath = path.join(
|
||||||
|
texthookerPath,
|
||||||
|
urlPath === "/" ? "index.html" : urlPath,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ext = path.extname(filePath);
|
||||||
|
const mimeTypes: Record<string, string> = {
|
||||||
|
".html": "text/html",
|
||||||
|
".js": "application/javascript",
|
||||||
|
".css": "text/css",
|
||||||
|
".json": "application/json",
|
||||||
|
".png": "image/png",
|
||||||
|
".svg": "image/svg+xml",
|
||||||
|
".ttf": "font/ttf",
|
||||||
|
".woff": "font/woff",
|
||||||
|
".woff2": "font/woff2",
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.readFile(filePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
res.writeHead(404);
|
||||||
|
res.end("Not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.writeHead(200, { "Content-Type": mimeTypes[ext] || "text/plain" });
|
||||||
|
res.end(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server.listen(port, "127.0.0.1", () => {
|
||||||
|
console.log(`Texthooker server running at http://127.0.0.1:${port}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
if (this.server) {
|
||||||
|
this.server.close();
|
||||||
|
this.server = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTexthookerPath(): string | null {
|
||||||
|
const searchPaths = [
|
||||||
|
path.join(__dirname, "..", "..", "..", "vendor", "texthooker-ui", "docs"),
|
||||||
|
path.join(
|
||||||
|
process.resourcesPath,
|
||||||
|
"app",
|
||||||
|
"vendor",
|
||||||
|
"texthooker-ui",
|
||||||
|
"docs",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (const candidate of searchPaths) {
|
||||||
|
if (fs.existsSync(path.join(candidate, "index.html"))) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
137
src/main.ts
137
src/main.ts
@@ -51,7 +51,6 @@ import * as https from "https";
|
|||||||
import * as os from "os";
|
import * as os from "os";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import WebSocket from "ws";
|
|
||||||
import { MecabTokenizer } from "./mecab-tokenizer";
|
import { MecabTokenizer } from "./mecab-tokenizer";
|
||||||
import { mergeTokens } from "./token-merger";
|
import { mergeTokens } from "./token-merger";
|
||||||
import { createWindowTracker, BaseWindowTracker } from "./window-trackers";
|
import { createWindowTracker, BaseWindowTracker } from "./window-trackers";
|
||||||
@@ -125,6 +124,11 @@ import {
|
|||||||
} from "./core/utils/electron-backend";
|
} from "./core/utils/electron-backend";
|
||||||
import { asBoolean, asFiniteNumber, asString } from "./core/utils/coerce";
|
import { asBoolean, asFiniteNumber, asString } from "./core/utils/coerce";
|
||||||
import { resolveKeybindings } from "./core/utils/keybindings";
|
import { resolveKeybindings } from "./core/utils/keybindings";
|
||||||
|
import { TexthookerService } from "./core/services/texthooker-service";
|
||||||
|
import {
|
||||||
|
hasMpvWebsocketPlugin,
|
||||||
|
SubtitleWebSocketService,
|
||||||
|
} from "./core/services/subtitle-ws-service";
|
||||||
import {
|
import {
|
||||||
ConfigService,
|
ConfigService,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
@@ -138,13 +142,13 @@ if (process.platform === "linux") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
const DEFAULT_TEXTHOOKER_PORT = 5174;
|
||||||
let texthookerServer: http.Server | null = null;
|
|
||||||
let subtitleWebSocketServer: WebSocket.Server | null = null;
|
|
||||||
const CONFIG_DIR = path.join(os.homedir(), ".config", "SubMiner");
|
const CONFIG_DIR = path.join(os.homedir(), ".config", "SubMiner");
|
||||||
const USER_DATA_PATH = CONFIG_DIR;
|
const USER_DATA_PATH = CONFIG_DIR;
|
||||||
const configService = new ConfigService(CONFIG_DIR);
|
const configService = new ConfigService(CONFIG_DIR);
|
||||||
const isDev =
|
const isDev =
|
||||||
process.argv.includes("--dev") || process.argv.includes("--debug");
|
process.argv.includes("--dev") || process.argv.includes("--debug");
|
||||||
|
const texthookerService = new TexthookerService();
|
||||||
|
const subtitleWsService = new SubtitleWebSocketService();
|
||||||
|
|
||||||
function getDefaultSocketPath(): string {
|
function getDefaultSocketPath(): string {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === "win32") {
|
||||||
@@ -503,115 +507,6 @@ function updateCurrentMediaPath(mediaPath: unknown): void {
|
|||||||
broadcastToOverlayWindows("subtitle-position:set", position);
|
broadcastToOverlayWindows("subtitle-position:set", position);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTexthookerPath(): string | null {
|
|
||||||
const searchPaths = [
|
|
||||||
path.join(__dirname, "..", "vendor", "texthooker-ui", "docs"),
|
|
||||||
path.join(process.resourcesPath, "app", "vendor", "texthooker-ui", "docs"),
|
|
||||||
];
|
|
||||||
for (const p of searchPaths) {
|
|
||||||
if (fs.existsSync(path.join(p, "index.html"))) {
|
|
||||||
return p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function startTexthookerServer(port: number): http.Server | null {
|
|
||||||
const texthookerPath = getTexthookerPath();
|
|
||||||
if (!texthookerPath) {
|
|
||||||
console.error("texthooker-ui not found");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
texthookerServer = http.createServer((req, res) => {
|
|
||||||
let urlPath = (req.url || "/").split("?")[0];
|
|
||||||
let filePath = path.join(
|
|
||||||
texthookerPath,
|
|
||||||
urlPath === "/" ? "index.html" : urlPath,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ext = path.extname(filePath);
|
|
||||||
const mimeTypes: Record<string, string> = {
|
|
||||||
".html": "text/html",
|
|
||||||
".js": "application/javascript",
|
|
||||||
".css": "text/css",
|
|
||||||
".json": "application/json",
|
|
||||||
".png": "image/png",
|
|
||||||
".svg": "image/svg+xml",
|
|
||||||
".ttf": "font/ttf",
|
|
||||||
".woff": "font/woff",
|
|
||||||
".woff2": "font/woff2",
|
|
||||||
};
|
|
||||||
|
|
||||||
fs.readFile(filePath, (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
res.writeHead(404);
|
|
||||||
res.end("Not found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.writeHead(200, { "Content-Type": mimeTypes[ext] || "text/plain" });
|
|
||||||
res.end(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
texthookerServer.listen(port, "127.0.0.1", () => {
|
|
||||||
console.log(`Texthooker server running at http://127.0.0.1:${port}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
return texthookerServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopTexthookerServer(): void {
|
|
||||||
if (texthookerServer) {
|
|
||||||
texthookerServer.close();
|
|
||||||
texthookerServer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMpvWebsocket(): boolean {
|
|
||||||
const mpvWebsocketPath = path.join(
|
|
||||||
os.homedir(),
|
|
||||||
".config",
|
|
||||||
"mpv",
|
|
||||||
"mpv_websocket",
|
|
||||||
);
|
|
||||||
return fs.existsSync(mpvWebsocketPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startSubtitleWebSocketServer(port: number): void {
|
|
||||||
subtitleWebSocketServer = new WebSocket.Server({ port, host: "127.0.0.1" });
|
|
||||||
|
|
||||||
subtitleWebSocketServer.on("connection", (ws: WebSocket) => {
|
|
||||||
console.log("WebSocket client connected");
|
|
||||||
if (currentSubText) {
|
|
||||||
ws.send(JSON.stringify({ sentence: currentSubText }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
subtitleWebSocketServer.on("error", (err: Error) => {
|
|
||||||
console.error("WebSocket server error:", err.message);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Subtitle WebSocket server running on ws://127.0.0.1:${port}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function broadcastSubtitle(text: string): void {
|
|
||||||
if (!subtitleWebSocketServer) return;
|
|
||||||
const message = JSON.stringify({ sentence: text });
|
|
||||||
for (const client of subtitleWebSocketServer.clients) {
|
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
|
||||||
client.send(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopSubtitleWebSocketServer(): void {
|
|
||||||
if (subtitleWebSocketServer) {
|
|
||||||
subtitleWebSocketServer.close();
|
|
||||||
subtitleWebSocketServer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
const AUTOSUBSYNC_SPINNER_FRAMES = ["|", "/", "-", "\\"];
|
||||||
let subsyncInProgress = false;
|
let subsyncInProgress = false;
|
||||||
|
|
||||||
@@ -723,9 +618,9 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
wsEnabled === true ||
|
wsEnabled === true ||
|
||||||
(wsEnabled === "auto" && !hasMpvWebsocket())
|
(wsEnabled === "auto" && !hasMpvWebsocketPlugin())
|
||||||
) {
|
) {
|
||||||
startSubtitleWebSocketServer(wsPort);
|
subtitleWsService.start(wsPort, () => currentSubText);
|
||||||
} else if (wsEnabled === "auto") {
|
} else if (wsEnabled === "auto") {
|
||||||
console.log(
|
console.log(
|
||||||
"mpv_websocket detected, skipping built-in WebSocket server",
|
"mpv_websocket detected, skipping built-in WebSocket server",
|
||||||
@@ -759,8 +654,8 @@ if (initialArgs.generateConfig && !shouldStartApp(initialArgs)) {
|
|||||||
|
|
||||||
app.on("will-quit", () => {
|
app.on("will-quit", () => {
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
stopSubtitleWebSocketServer();
|
subtitleWsService.stop();
|
||||||
stopTexthookerServer();
|
texthookerService.stop();
|
||||||
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
if (yomitanParserWindow && !yomitanParserWindow.isDestroyed()) {
|
||||||
yomitanParserWindow.destroy();
|
yomitanParserWindow.destroy();
|
||||||
}
|
}
|
||||||
@@ -848,7 +743,7 @@ function handleCliCommand(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (args.texthookerPort !== undefined) {
|
if (args.texthookerPort !== undefined) {
|
||||||
if (texthookerServer) {
|
if (texthookerService.isRunning()) {
|
||||||
console.warn(
|
console.warn(
|
||||||
"Ignoring --port override because the texthooker server is already running.",
|
"Ignoring --port override because the texthooker server is already running.",
|
||||||
);
|
);
|
||||||
@@ -925,8 +820,8 @@ function handleCliCommand(
|
|||||||
} else if (args.openRuntimeOptions) {
|
} else if (args.openRuntimeOptions) {
|
||||||
openRuntimeOptionsPalette();
|
openRuntimeOptionsPalette();
|
||||||
} else if (args.texthooker) {
|
} else if (args.texthooker) {
|
||||||
if (!texthookerServer) {
|
if (!texthookerService.isRunning()) {
|
||||||
startTexthookerServer(texthookerPort);
|
texthookerService.start(texthookerPort);
|
||||||
}
|
}
|
||||||
const config = getResolvedConfig();
|
const config = getResolvedConfig();
|
||||||
const openBrowser = config.texthooker?.openBrowser !== false;
|
const openBrowser = config.texthooker?.openBrowser !== false;
|
||||||
@@ -1255,7 +1150,7 @@ class MpvIpcClient implements MpvClient {
|
|||||||
this.currentSubEnd,
|
this.currentSubEnd,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
broadcastSubtitle(currentSubText);
|
subtitleWsService.broadcast(currentSubText);
|
||||||
if (getOverlayWindows().length > 0) {
|
if (getOverlayWindows().length > 0) {
|
||||||
const subtitleData = await tokenizeSubtitle(currentSubText);
|
const subtitleData = await tokenizeSubtitle(currentSubText);
|
||||||
broadcastToOverlayWindows("subtitle:set", subtitleData);
|
broadcastToOverlayWindows("subtitle:set", subtitleData);
|
||||||
@@ -1424,7 +1319,7 @@ class MpvIpcClient implements MpvClient {
|
|||||||
if (mpvClient) {
|
if (mpvClient) {
|
||||||
mpvClient.currentSubText = currentSubText;
|
mpvClient.currentSubText = currentSubText;
|
||||||
}
|
}
|
||||||
broadcastSubtitle(currentSubText);
|
subtitleWsService.broadcast(currentSubText);
|
||||||
if (getOverlayWindows().length > 0) {
|
if (getOverlayWindows().length > 0) {
|
||||||
tokenizeSubtitle(currentSubText).then((subtitleData) => {
|
tokenizeSubtitle(currentSubText).then((subtitleData) => {
|
||||||
broadcastToOverlayWindows("subtitle:set", subtitleData);
|
broadcastToOverlayWindows("subtitle:set", subtitleData);
|
||||||
|
|||||||
Reference in New Issue
Block a user