diff --git a/Dockerfile b/Dockerfile index 7b4e487..b5a1fea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,20 @@ FROM node:20-slim AS builder +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package.json tsconfig.json ./ RUN npm install +# Build the git dependency (filename-tools) +RUN cd node_modules/@metazla/filename-tools && npm install && npm run build COPY src/ ./src/ RUN npm run build FROM node:20-slim +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package.json ./ RUN npm install --omit=dev +# Build the git dependency in production too +RUN cd node_modules/@metazla/filename-tools && npm install && npm run build COPY --from=builder /app/dist ./dist EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ diff --git a/package.json b/package.json index 29c7960..0821133 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "type": "module", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js" }, - "dependencies": { "fastify": "^5.0.0", "iso-639-1": "^3.1.2", "iso-639-3": "^3.0.1" }, + "dependencies": { + "fastify": "^5.0.0", + "@metazla/filename-tools": "github:worph/filename-tool" + }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.3.0" } } diff --git a/src/index.ts b/src/index.ts index a1db0dc..a532e17 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,26 @@ +/** + * MetaMesh Plugin: language + * + * ============================================================================ + * 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/ (READ-WRITE) - Plugin output folder (if needed) + * + * This plugin (language) only requires mount 2: + * - /cache (RW) - to cache language aggregation results + * - Note: This plugin aggregates metadata from ffmpeg, doesn't read files + * + * SECURITY: Plugins must NEVER write to /files directly. + * All write operations go to /cache or /files/plugin/ only. + * + * ============================================================================ + */ + import Fastify from 'fastify'; import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js'; import { manifest, process as processFile } from './plugin.js'; diff --git a/src/plugin.ts b/src/plugin.ts index ebabc6c..90d3526 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,11 +1,16 @@ /** * Language Plugin - * Aggregates languages from all streams and determines primary language - * Uses proper ISO 639 conversion matching @metazla/filename-tools + * + * Aggregates languages from all streams (audio, video, subtitle) + * and converts them to ISO 639-3 format. + * Determines the primary language based on first audio > video > subtitle stream. + * + * Matches old LanguageProcessor output: + * - languages (add) + * - titles/{lang} (set originalTitle) */ -import ISO6391 from 'iso-639-1'; -import { iso6393To2T } from 'iso-639-3'; +import { anyTo_iso_639_3 } from '@metazla/filename-tools'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import { MetaCoreClient } from './meta-core-client.js'; @@ -22,45 +27,10 @@ export const manifest: PluginManifest = { timeout: 30000, schema: { languages: { label: 'Languages', type: 'array', readonly: true }, - primaryLanguage: { label: 'Primary Language', type: 'string', readonly: true }, }, config: {}, }; -// Build ISO 639-1 to ISO 639-3 mapping (matching @metazla/filename-tools) -const iso6391To3: Record = {}; -for (const key in iso6393To2T) { - const value = (iso6393To2T as Record)[key]; - iso6391To3[value] = key; -} - -/** - * Convert any language format to ISO 639-3 - * Matches anyTo_iso_639_3 from @metazla/filename-tools - */ -function anyTo_iso_639_3(languageName: string): string | null { - if (!languageName) { - return null; - } - if (languageName.length === 3) { - // Could be ISO 639-3 or ISO 639-2T or ISO 639-2B - // All 3 are similar, just return the input - return languageName; - } - if (languageName.length === 2) { - // Could be ISO 639-1 => validate and convert - return iso6391To3[languageName] || 'und'; - } - - // Full language name - convert to ISO 639-3 - const code1 = ISO6391.getCode(languageName); - if (iso6391To3[code1]) { - return iso6391To3[code1]; - } - - return null; -} - export async function process( request: ProcessRequest, sendCallback: (payload: CallbackPayload) => Promise @@ -71,56 +41,67 @@ export async function process( try { const { cid, existingMeta } = request; - const languages = new Set(); - let primaryLanguage: string | null = null; + let streamLanguage: string | null = null; - // Collect from audio streams (highest priority) + // Collect languages from title keys (if existingMeta has titles/*) + for (const key of Object.keys(existingMeta || {})) { + if (key.startsWith('titles/')) { + const langCode = key.substring(7); // Remove 'titles/' prefix + const normalized = anyTo_iso_639_3(langCode); + if (normalized) { + await metaCore.addToSet(cid, 'languages', normalized); + } + } + } + + // Collect languages from audio streams (highest priority for primary) for (let i = 0; i < 20; i++) { const lang = existingMeta?.[`fileinfo/streamdetails/audio/${i}/language`]; if (!lang) break; const computedLanguage = anyTo_iso_639_3(lang); - if (computedLanguage && computedLanguage !== 'und') { - languages.add(computedLanguage); - if (!primaryLanguage) primaryLanguage = computedLanguage; + if (computedLanguage) { + await metaCore.addToSet(cid, 'languages', computedLanguage); + if (!streamLanguage) { + streamLanguage = computedLanguage; + } } } - // Collect from video streams + // Collect languages from video streams for (let i = 0; i < 20; i++) { const lang = existingMeta?.[`fileinfo/streamdetails/video/${i}/language`]; if (!lang) break; const computedLanguage = anyTo_iso_639_3(lang); - if (computedLanguage && computedLanguage !== 'und') { - languages.add(computedLanguage); - if (!primaryLanguage) primaryLanguage = computedLanguage; + if (computedLanguage) { + await metaCore.addToSet(cid, 'languages', computedLanguage); + if (!streamLanguage) { + streamLanguage = computedLanguage; + } } } - // Collect from subtitle streams + // Collect languages from subtitle streams for (let i = 0; i < 20; i++) { const lang = existingMeta?.[`fileinfo/streamdetails/subtitle/${i}/language`]; if (!lang) break; const computedLanguage = anyTo_iso_639_3(lang); - if (computedLanguage && computedLanguage !== 'und') { - languages.add(computedLanguage); - if (!primaryLanguage) primaryLanguage = computedLanguage; + if (computedLanguage) { + await metaCore.addToSet(cid, 'languages', computedLanguage); + if (!streamLanguage) { + streamLanguage = computedLanguage; + } } } - // Add languages to set - for (const lang of languages) { - await metaCore.addToSet(cid, 'languages', lang); + // Determine the language of the media file based on: + // 1st audio stream, 2nd video stream, 3rd subtitle stream (first found) + // Assume original title is one of the stream languages or English if not found + const originalTitle = existingMeta?.originalTitle; + if (originalTitle) { + await metaCore.setProperty(cid, `titles/${streamLanguage || 'eng'}`, originalTitle); } - // Set primary language and title language mapping - if (primaryLanguage) { - await metaCore.setProperty(cid, 'primaryLanguage', primaryLanguage); - - const originalTitle = existingMeta?.originalTitle; - if (originalTitle) { - await metaCore.setProperty(cid, `titles/${primaryLanguage}`, originalTitle); - } - } + console.log(`[language] Aggregated languages, primary: ${streamLanguage || 'none'}`); await sendCallback({ taskId: request.taskId,