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 6f55795..352c8d3 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,10 @@ "type": "module", "main": "dist/index.js", "scripts": { "build": "tsc", "start": "node dist/index.js" }, - "dependencies": { "fastify": "^5.0.0", "bencode": "^4.0.0" }, + "dependencies": { + "fastify": "^5.0.0", + "bencode": "^4.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..8b15d9a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,26 @@ +/** + * MetaMesh Plugin: torrent + * + * ============================================================================ + * 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 (torrent) only requires mounts 1 and 2: + * - /files (RO) - to read .torrent files + * - /cache (RW) - to cache parsed torrent metadata + * + * 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 3cf7524..4ea41ac 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,13 +1,35 @@ /** * Torrent Plugin - * Parses .torrent files and extracts metadata + * + * Parses .torrent files and extracts metadata including: + * - Announce URLs + * - Torrent name and files + * - Creation info + * - Video file metadata from the torrent + * + * Matches old TorrentProcessor output: + * - announce, announceList (add), comment, createdBy, creationDate + * - info/files/{n}/length, info/files/{n}/path, info/name */ import { readFile } from 'fs/promises'; import bencode from 'bencode'; +import { FileType, FileNameVideoMetaExtractor, episodePatterns, seasonAndEpisodePatterns, seasonPatterns, extraEpKeyWords, keywordsArray, substringArray, soloEp } from '@metazla/filename-tools'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import { MetaCoreClient } from './meta-core-client.js'; +const fileType = new FileType(); +const fileNameMetaExtractor = new FileNameVideoMetaExtractor( + [], + episodePatterns, + seasonAndEpisodePatterns, + seasonPatterns, + extraEpKeyWords, + keywordsArray, + substringArray, + soloEp +); + export const manifest: PluginManifest = { id: 'torrent', name: 'Torrent Parser', @@ -39,6 +61,7 @@ export async function process( try { const { cid, filePath, existingMeta } = request; + // Only process torrent files if (existingMeta?.fileType !== 'torrent') { await sendCallback({ taskId: request.taskId, @@ -49,35 +72,57 @@ export async function process( return; } + // Read and parse torrent file const torrentData = await readFile(filePath); const parsed = bencode.decode(torrentData, 'utf8') as Record; const metadata: Record = {}; - // Main announce URL + // Getting the filename and try to parse video info + let fileName = 'undefined'; + try { + fileName = parsed.info?.name?.toString?.() || 'undefined'; + const torrentFileType = fileType.getFileTypeFromExtension(fileName); + if (torrentFileType === 'video') { + // Use the same method as old processor - extract video metadata from torrent name + const videoMeta = fileNameMetaExtractor.extractVideoFileMetadata(fileName); + if (videoMeta.originalTitle) metadata.originalTitle = videoMeta.originalTitle; + if (videoMeta.season) metadata.season = videoMeta.season; + if (videoMeta.episode) metadata.episode = videoMeta.episode; + if (videoMeta.movieYear) metadata.movieYear = videoMeta.movieYear; + if (videoMeta.videoType) metadata.videoType = videoMeta.videoType; + } + } catch (e) { + // Ignore multi-file torrent parsing errors + console.debug(`[torrent] Could not parse video metadata from torrent name: ${e}`); + } + + // Extract main announce URL const announce = parsed.announce?.toString?.(); - if (announce) metadata.announce = announce; + if (announce) { + metadata.announce = announce; + } - // Torrent name - const name = parsed.info?.name?.toString?.(); - if (name) metadata['info/name'] = name; - - // Optional metadata + // Extract optional metadata const comment = parsed.comment?.toString?.(); - if (comment) metadata.comment = comment; + if (comment) { + metadata.comment = comment; + } const createdBy = parsed['created by']?.toString?.(); - if (createdBy) metadata.createdBy = createdBy; + if (createdBy) { + metadata.createdBy = createdBy; + } const creationDate = parsed['creation date']; if (creationDate != null) { metadata.creationDate = String(creationDate); } - // Files info - const files = parsed.info?.files; - if (files) { + // Extract info section - files + try { + const files = parsed.info?.files; let i = 0; - for (const key in files) { + for (const key in files || {}) { const file = files[key]; const length = file.length; const pathParts = file.path; @@ -88,13 +133,21 @@ export async function process( metadata[`info/files/${i}/length`] = String(length); metadata[`info/files/${i}/path`] = pathStr; i++; - if (i > 100) break; // Limit files + if (i > 100) break; // Limit files to prevent DoS } + } catch (e) { + console.debug(`[torrent] Error parsing torrent files: ${e}`); + } + + // Set torrent name + const name = parsed.info?.name?.toString?.(); + if (name) { + metadata['info/name'] = name; } await metaCore.mergeMetadata(cid, metadata); - // Add announce list to set + // Extract announce list (trackers) using add for RecordSet const announceList = parsed['announce-list'] || []; for (const tier of announceList) { for (const tracker of tier || []) { @@ -104,12 +157,15 @@ export async function process( } } + console.log(`[torrent] Parsed torrent file: ${filePath}`); + await sendCallback({ taskId: request.taskId, status: 'completed', duration: Date.now() - startTime, }); } catch (error) { + console.error(`[torrent] Error parsing torrent file ${request.filePath}:`, error); await sendCallback({ taskId: request.taskId, status: 'failed',