mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
refactor: extract jimaku, subtitle position, and render metric services
This commit is contained in:
81
src/core/services/jimaku-runtime-service.ts
Normal file
81
src/core/services/jimaku-runtime-service.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
JimakuApiResponse,
|
||||
JimakuConfig,
|
||||
JimakuLanguagePreference,
|
||||
} from "../../types";
|
||||
import {
|
||||
jimakuFetchJson as jimakuFetchJsonRequest,
|
||||
resolveJimakuApiKey as resolveJimakuApiKeyFromConfig,
|
||||
} from "../../jimaku/utils";
|
||||
|
||||
export function getJimakuConfigService(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
): JimakuConfig {
|
||||
const config = getResolvedConfig();
|
||||
return config.jimaku ?? {};
|
||||
}
|
||||
|
||||
export function getJimakuBaseUrlService(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultBaseUrl: string,
|
||||
): string {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
return config.apiBaseUrl || defaultBaseUrl;
|
||||
}
|
||||
|
||||
export function getJimakuLanguagePreferenceService(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultPreference: JimakuLanguagePreference,
|
||||
): JimakuLanguagePreference {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
return config.languagePreference || defaultPreference;
|
||||
}
|
||||
|
||||
export function getJimakuMaxEntryResultsService(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
defaultValue: number,
|
||||
): number {
|
||||
const config = getJimakuConfigService(getResolvedConfig);
|
||||
const value = config.maxEntryResults;
|
||||
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
export async function resolveJimakuApiKeyService(
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig },
|
||||
): Promise<string | null> {
|
||||
return resolveJimakuApiKeyFromConfig(getJimakuConfigService(getResolvedConfig));
|
||||
}
|
||||
|
||||
export async function jimakuFetchJsonService<T>(
|
||||
endpoint: string,
|
||||
query: Record<string, string | number | boolean | null | undefined> = {},
|
||||
options: {
|
||||
getResolvedConfig: () => { jimaku?: JimakuConfig };
|
||||
defaultBaseUrl: string;
|
||||
defaultMaxEntryResults: number;
|
||||
defaultLanguagePreference: JimakuLanguagePreference;
|
||||
},
|
||||
): Promise<JimakuApiResponse<T>> {
|
||||
const apiKey = await resolveJimakuApiKeyService(options.getResolvedConfig);
|
||||
if (!apiKey) {
|
||||
return {
|
||||
ok: false,
|
||||
error: {
|
||||
error:
|
||||
"Jimaku API key not set. Configure jimaku.apiKey or jimaku.apiKeyCommand.",
|
||||
code: 401,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return jimakuFetchJsonRequest<T>(endpoint, query, {
|
||||
baseUrl: getJimakuBaseUrlService(
|
||||
options.getResolvedConfig,
|
||||
options.defaultBaseUrl,
|
||||
),
|
||||
apiKey,
|
||||
});
|
||||
}
|
||||
57
src/core/services/mpv-render-metrics-service.ts
Normal file
57
src/core/services/mpv-render-metrics-service.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { MpvSubtitleRenderMetrics } from "../../types";
|
||||
import { asBoolean, asFiniteNumber, asString } from "../utils/coerce";
|
||||
|
||||
export function updateMpvSubtitleRenderMetricsService(
|
||||
current: MpvSubtitleRenderMetrics,
|
||||
patch: Partial<MpvSubtitleRenderMetrics>,
|
||||
): MpvSubtitleRenderMetrics {
|
||||
const patchOsd = patch.osdDimensions;
|
||||
const nextOsdDimensions =
|
||||
patchOsd &&
|
||||
typeof patchOsd.w === "number" &&
|
||||
typeof patchOsd.h === "number" &&
|
||||
typeof patchOsd.ml === "number" &&
|
||||
typeof patchOsd.mr === "number" &&
|
||||
typeof patchOsd.mt === "number" &&
|
||||
typeof patchOsd.mb === "number"
|
||||
? {
|
||||
w: asFiniteNumber(patchOsd.w, 0, 1, 100000),
|
||||
h: asFiniteNumber(patchOsd.h, 0, 1, 100000),
|
||||
ml: asFiniteNumber(patchOsd.ml, 0, 0, 100000),
|
||||
mr: asFiniteNumber(patchOsd.mr, 0, 0, 100000),
|
||||
mt: asFiniteNumber(patchOsd.mt, 0, 0, 100000),
|
||||
mb: asFiniteNumber(patchOsd.mb, 0, 0, 100000),
|
||||
}
|
||||
: patchOsd === null
|
||||
? null
|
||||
: current.osdDimensions;
|
||||
|
||||
return {
|
||||
subPos: asFiniteNumber(patch.subPos, current.subPos, 0, 150),
|
||||
subFontSize: asFiniteNumber(patch.subFontSize, current.subFontSize, 1, 200),
|
||||
subScale: asFiniteNumber(patch.subScale, current.subScale, 0.1, 10),
|
||||
subMarginY: asFiniteNumber(patch.subMarginY, current.subMarginY, 0, 200),
|
||||
subMarginX: asFiniteNumber(patch.subMarginX, current.subMarginX, 0, 200),
|
||||
subFont: asString(patch.subFont, current.subFont),
|
||||
subSpacing: asFiniteNumber(patch.subSpacing, current.subSpacing, -100, 100),
|
||||
subBold: asBoolean(patch.subBold, current.subBold),
|
||||
subItalic: asBoolean(patch.subItalic, current.subItalic),
|
||||
subBorderSize: asFiniteNumber(
|
||||
patch.subBorderSize,
|
||||
current.subBorderSize,
|
||||
0,
|
||||
100,
|
||||
),
|
||||
subShadowOffset: asFiniteNumber(
|
||||
patch.subShadowOffset,
|
||||
current.subShadowOffset,
|
||||
0,
|
||||
100,
|
||||
),
|
||||
subAssOverride: asString(patch.subAssOverride, current.subAssOverride),
|
||||
subScaleByWindow: asBoolean(patch.subScaleByWindow, current.subScaleByWindow),
|
||||
subUseMargins: asBoolean(patch.subUseMargins, current.subUseMargins),
|
||||
osdHeight: asFiniteNumber(patch.osdHeight, current.osdHeight, 1, 10000),
|
||||
osdDimensions: nextOsdDimensions,
|
||||
};
|
||||
}
|
||||
154
src/core/services/subtitle-position-service.ts
Normal file
154
src/core/services/subtitle-position-service.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import * as crypto from "crypto";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { SubtitlePosition } from "../../types";
|
||||
|
||||
function getSubtitlePositionFilePath(
|
||||
mediaPath: string,
|
||||
subtitlePositionsDir: string,
|
||||
): string {
|
||||
const key = normalizeMediaPathForSubtitlePosition(mediaPath);
|
||||
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
||||
return path.join(subtitlePositionsDir, `${hash}.json`);
|
||||
}
|
||||
|
||||
function normalizeMediaPathForSubtitlePosition(mediaPath: string): string {
|
||||
const trimmed = mediaPath.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
if (
|
||||
/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ||
|
||||
/^ytsearch:/.test(trimmed)
|
||||
) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const resolved = path.resolve(trimmed);
|
||||
let normalized = resolved;
|
||||
try {
|
||||
if (fs.existsSync(resolved)) {
|
||||
normalized = fs.realpathSync(resolved);
|
||||
}
|
||||
} catch {
|
||||
normalized = resolved;
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
normalized = normalized.toLowerCase();
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function persistSubtitlePosition(
|
||||
position: SubtitlePosition,
|
||||
currentMediaPath: string | null,
|
||||
subtitlePositionsDir: string,
|
||||
): void {
|
||||
if (!currentMediaPath) return;
|
||||
if (!fs.existsSync(subtitlePositionsDir)) {
|
||||
fs.mkdirSync(subtitlePositionsDir, { recursive: true });
|
||||
}
|
||||
const positionPath = getSubtitlePositionFilePath(
|
||||
currentMediaPath,
|
||||
subtitlePositionsDir,
|
||||
);
|
||||
fs.writeFileSync(positionPath, JSON.stringify(position, null, 2));
|
||||
}
|
||||
|
||||
export function loadSubtitlePositionService(options: {
|
||||
currentMediaPath: string | null;
|
||||
fallbackPosition: SubtitlePosition;
|
||||
} & { subtitlePositionsDir: string }): SubtitlePosition | null {
|
||||
if (!options.currentMediaPath) {
|
||||
return options.fallbackPosition;
|
||||
}
|
||||
|
||||
try {
|
||||
const positionPath = getSubtitlePositionFilePath(
|
||||
options.currentMediaPath,
|
||||
options.subtitlePositionsDir,
|
||||
);
|
||||
if (!fs.existsSync(positionPath)) {
|
||||
return options.fallbackPosition;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(positionPath, "utf-8");
|
||||
const parsed = JSON.parse(data) as Partial<SubtitlePosition>;
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed.yPercent === "number" &&
|
||||
Number.isFinite(parsed.yPercent)
|
||||
) {
|
||||
return { yPercent: parsed.yPercent };
|
||||
}
|
||||
return options.fallbackPosition;
|
||||
} catch (err) {
|
||||
console.error("Failed to load subtitle position:", (err as Error).message);
|
||||
return options.fallbackPosition;
|
||||
}
|
||||
}
|
||||
|
||||
export function saveSubtitlePositionService(options: {
|
||||
position: SubtitlePosition;
|
||||
currentMediaPath: string | null;
|
||||
subtitlePositionsDir: string;
|
||||
onQueuePending: (position: SubtitlePosition) => void;
|
||||
onPersisted: () => void;
|
||||
}): void {
|
||||
if (!options.currentMediaPath) {
|
||||
options.onQueuePending(options.position);
|
||||
console.warn("Queued subtitle position save - no media path yet");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
persistSubtitlePosition(
|
||||
options.position,
|
||||
options.currentMediaPath,
|
||||
options.subtitlePositionsDir,
|
||||
);
|
||||
options.onPersisted();
|
||||
} catch (err) {
|
||||
console.error("Failed to save subtitle position:", (err as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateCurrentMediaPathService(options: {
|
||||
mediaPath: unknown;
|
||||
currentMediaPath: string | null;
|
||||
pendingSubtitlePosition: SubtitlePosition | null;
|
||||
subtitlePositionsDir: string;
|
||||
loadSubtitlePosition: () => SubtitlePosition | null;
|
||||
setCurrentMediaPath: (mediaPath: string | null) => void;
|
||||
clearPendingSubtitlePosition: () => void;
|
||||
setSubtitlePosition: (position: SubtitlePosition | null) => void;
|
||||
broadcastSubtitlePosition: (position: SubtitlePosition | null) => void;
|
||||
}): void {
|
||||
const nextPath =
|
||||
typeof options.mediaPath === "string" && options.mediaPath.trim().length > 0
|
||||
? options.mediaPath
|
||||
: null;
|
||||
if (nextPath === options.currentMediaPath) return;
|
||||
options.setCurrentMediaPath(nextPath);
|
||||
|
||||
if (nextPath && options.pendingSubtitlePosition) {
|
||||
try {
|
||||
persistSubtitlePosition(
|
||||
options.pendingSubtitlePosition,
|
||||
nextPath,
|
||||
options.subtitlePositionsDir,
|
||||
);
|
||||
options.setSubtitlePosition(options.pendingSubtitlePosition);
|
||||
options.clearPendingSubtitlePosition();
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Failed to persist queued subtitle position:",
|
||||
(err as Error).message,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const position = options.loadSubtitlePosition();
|
||||
options.broadcastSubtitlePosition(position);
|
||||
}
|
||||
Reference in New Issue
Block a user