bugfix
This commit is contained in:
@@ -1,3 +1,26 @@
|
||||
/**
|
||||
* MetaMesh Plugin: subtitle
|
||||
*
|
||||
* ============================================================================
|
||||
* PLUGIN MOUNT ARCHITECTURE - DO NOT MODIFY WITHOUT AUTHORIZATION
|
||||
* ============================================================================
|
||||
*
|
||||
* Each plugin container has exactly 3 mounts (2 for plugins without output):
|
||||
*
|
||||
* 1. /files (READ-ONLY) - Shared media files, read access only
|
||||
* 2. /cache (READ-WRITE) - Plugin-specific cache folder
|
||||
* 3. /files/plugin/<id> (READ-WRITE) - Plugin output folder (if needed)
|
||||
*
|
||||
* This plugin (subtitle) only requires mounts 1 and 2:
|
||||
* - /files (RO) - to read subtitle files alongside media
|
||||
* - /cache (RW) - to cache subtitle metadata
|
||||
*
|
||||
* SECURITY: Plugins must NEVER write to /files directly.
|
||||
* All write operations go to /cache or /files/plugin/<id> only.
|
||||
*
|
||||
* ============================================================================
|
||||
*/
|
||||
|
||||
import Fastify from 'fastify';
|
||||
import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js';
|
||||
import { manifest, process as processFile } from './plugin.js';
|
||||
|
||||
@@ -70,4 +70,24 @@ export class MetaCoreClient {
|
||||
const data = await response.json() as { metadata?: Record<string, string> };
|
||||
return data.metadata ?? {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the CID (Content Identifier) for a file
|
||||
* Uses the meta-core /file/cid API endpoint
|
||||
*/
|
||||
async computeFileCID(filePath: string): Promise<string | null> {
|
||||
try {
|
||||
const response = await this.safeFetch(`${this.baseUrl}/file/cid`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path: filePath }),
|
||||
});
|
||||
if (!response || !response.ok) return null;
|
||||
const data = await response.json() as { cid?: string };
|
||||
return data.cid ?? null;
|
||||
} catch (error) {
|
||||
console.debug(`[MetaCoreClient] Failed to compute CID for ${filePath}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+105
-48
@@ -1,14 +1,23 @@
|
||||
/**
|
||||
* Subtitle Plugin
|
||||
* Processes subtitle files and links them to videos
|
||||
*
|
||||
* For subtitle files: finds sibling video files and detects language.
|
||||
* For video files: finds sibling subtitle files.
|
||||
*
|
||||
* Matches old SubtitleProcessor output:
|
||||
* - For subtitle files: videos (add CID), subtitleLanguage
|
||||
* - For video files: subtitles (add CID)
|
||||
*/
|
||||
|
||||
import { readFile, access, readdir } from 'fs/promises';
|
||||
import { dirname, basename, extname, join } from 'path';
|
||||
import { readFile, access, open } from 'fs/promises';
|
||||
import { dirname, extname } from 'path';
|
||||
import { franc } from 'franc-min';
|
||||
import { FileType, getSiblingFiles } from '@metazla/filename-tools';
|
||||
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
|
||||
import { MetaCoreClient } from './meta-core-client.js';
|
||||
|
||||
const fileType = new FileType();
|
||||
|
||||
export const manifest: PluginManifest = {
|
||||
id: 'subtitle',
|
||||
name: 'Subtitle Processor',
|
||||
@@ -28,9 +37,6 @@ export const manifest: PluginManifest = {
|
||||
config: {},
|
||||
};
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set(['mp4', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'webm', 'm4v']);
|
||||
const SUBTITLE_EXTENSIONS = new Set(['srt', 'ass', 'ssa', 'vtt', 'sub']);
|
||||
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
@@ -40,6 +46,20 @@ async function fileExists(path: string): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read first N bytes of a file
|
||||
*/
|
||||
async function readFirstData(filePath: string, encoding: BufferEncoding = 'utf8', size = 1024): Promise<string> {
|
||||
const handle = await open(filePath, 'r');
|
||||
try {
|
||||
const buffer = Buffer.alloc(size);
|
||||
await handle.read(buffer, 0, size, 0);
|
||||
return buffer.toString(encoding);
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
}
|
||||
|
||||
export async function process(
|
||||
request: ProcessRequest,
|
||||
sendCallback: (payload: CallbackPayload) => Promise<void>
|
||||
@@ -49,49 +69,12 @@ export async function process(
|
||||
|
||||
try {
|
||||
const { cid, filePath, existingMeta } = request;
|
||||
const fileType = existingMeta?.fileType;
|
||||
const currentFileType = existingMeta?.fileType;
|
||||
|
||||
if (fileType === 'subtitle') {
|
||||
// For subtitle files: detect language and find sibling videos
|
||||
try {
|
||||
const content = await readFile(filePath, { encoding: 'utf8', flag: 'r' });
|
||||
const sample = content.slice(0, 4096);
|
||||
const lang = franc(sample);
|
||||
if (lang && lang !== 'und') {
|
||||
await metaCore.setProperty(cid, 'subtitleLanguage', lang);
|
||||
}
|
||||
} catch {
|
||||
// Ignore read errors
|
||||
}
|
||||
|
||||
// Find sibling videos
|
||||
const dir = dirname(filePath);
|
||||
const baseName = basename(filePath, extname(filePath));
|
||||
try {
|
||||
const files = await readdir(dir);
|
||||
for (const file of files) {
|
||||
const ext = extname(file).slice(1).toLowerCase();
|
||||
if (VIDEO_EXTENSIONS.has(ext)) {
|
||||
const videoBase = basename(file, extname(file));
|
||||
// Check if video name matches subtitle base name
|
||||
if (videoBase === baseName || baseName.startsWith(videoBase)) {
|
||||
// We don't have CID for sibling, just note the relationship
|
||||
await metaCore.setProperty(cid, 'linkedVideoName', file);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore directory read errors
|
||||
}
|
||||
} else if (fileType === 'video') {
|
||||
// For video files: find sibling subtitles
|
||||
const extension = existingMeta?.extension || '';
|
||||
for (const subExt of ['srt', 'ass', 'ssa', 'vtt']) {
|
||||
const subPath = filePath.replace(new RegExp(`\\.${extension}$`, 'i'), `.${subExt}`);
|
||||
if (await fileExists(subPath)) {
|
||||
await metaCore.setProperty(cid, `subtitleFile/${subExt}`, basename(subPath));
|
||||
}
|
||||
}
|
||||
if (currentFileType === 'subtitle') {
|
||||
await processSubtitleFile(metaCore, cid, filePath);
|
||||
} else if (currentFileType === 'video') {
|
||||
await processVideoFile(metaCore, cid, filePath, existingMeta);
|
||||
}
|
||||
|
||||
await sendCallback({
|
||||
@@ -108,3 +91,77 @@ export async function process(
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process subtitle file: find sibling videos and detect language
|
||||
*/
|
||||
async function processSubtitleFile(
|
||||
metaCore: MetaCoreClient,
|
||||
cid: string,
|
||||
filePath: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Find sibling video files
|
||||
const siblings = await getSiblingFiles(filePath);
|
||||
|
||||
for (const siblingPath of siblings) {
|
||||
const siblingType = fileType.getFileTypeFromExtension(siblingPath);
|
||||
if (siblingType === 'video') {
|
||||
try {
|
||||
const videoCid = await metaCore.computeFileCID(siblingPath);
|
||||
if (videoCid) {
|
||||
await metaCore.addToSet(cid, 'videos', videoCid);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`[subtitle] Could not get CID for video: ${siblingPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect subtitle language by reading first 4KB
|
||||
try {
|
||||
const content = await readFirstData(filePath, 'utf8', 4096);
|
||||
const detectedLang = franc(content);
|
||||
if (detectedLang && detectedLang !== 'und') {
|
||||
await metaCore.setProperty(cid, 'subtitleLanguage', detectedLang);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`[subtitle] Could not detect language for: ${filePath}`);
|
||||
}
|
||||
|
||||
console.log(`[subtitle] Processed subtitle file: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error(`[subtitle] Error processing subtitle file ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process video file: find sibling subtitle files
|
||||
*/
|
||||
async function processVideoFile(
|
||||
metaCore: MetaCoreClient,
|
||||
cid: string,
|
||||
filePath: string,
|
||||
existingMeta?: Record<string, string>
|
||||
): Promise<void> {
|
||||
try {
|
||||
const extension = existingMeta?.extension || '';
|
||||
const srtPath = filePath.replace(new RegExp(`\\.${extension}$`, 'i'), '.srt');
|
||||
|
||||
// Check for .srt subtitle
|
||||
if (await fileExists(srtPath)) {
|
||||
try {
|
||||
const subtitleCid = await metaCore.computeFileCID(srtPath);
|
||||
if (subtitleCid) {
|
||||
await metaCore.addToSet(cid, 'subtitles', subtitleCid);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`[subtitle] Could not get CID for subtitle: ${srtPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[subtitle] Checked subtitles for video: ${filePath}`);
|
||||
} catch (error) {
|
||||
console.error(`[subtitle] Error finding subtitles for ${filePath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user