This commit is contained in:
Pierre
2026-01-10 19:15:43 +01:00
parent 445a8c58a2
commit 06ee5a3dd0
4 changed files with 106 additions and 17 deletions
+6
View File
@@ -1,14 +1,20 @@
FROM node:20-slim AS builder FROM node:20-slim AS builder
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY package.json tsconfig.json ./ COPY package.json tsconfig.json ./
RUN npm install RUN npm install
# Build the git dependency (filename-tools)
RUN cd node_modules/@metazla/filename-tools && npm install && npm run build
COPY src/ ./src/ COPY src/ ./src/
RUN npm run build RUN npm run build
FROM node:20-slim FROM node:20-slim
RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
COPY package.json ./ COPY package.json ./
RUN npm install --omit=dev 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 COPY --from=builder /app/dist ./dist
EXPOSE 8080 EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
+5 -1
View File
@@ -5,6 +5,10 @@
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "build": "tsc", "start": "node 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" } "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.3.0" }
} }
+23
View File
@@ -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/<id> (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/<id> only.
*
* ============================================================================
*/
import Fastify from 'fastify'; import Fastify from 'fastify';
import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js'; import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js';
import { manifest, process as processFile } from './plugin.js'; import { manifest, process as processFile } from './plugin.js';
+71 -15
View File
@@ -1,13 +1,35 @@
/** /**
* Torrent Plugin * 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 { readFile } from 'fs/promises';
import bencode from 'bencode'; 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 type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
import { MetaCoreClient } from './meta-core-client.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 = { export const manifest: PluginManifest = {
id: 'torrent', id: 'torrent',
name: 'Torrent Parser', name: 'Torrent Parser',
@@ -39,6 +61,7 @@ export async function process(
try { try {
const { cid, filePath, existingMeta } = request; const { cid, filePath, existingMeta } = request;
// Only process torrent files
if (existingMeta?.fileType !== 'torrent') { if (existingMeta?.fileType !== 'torrent') {
await sendCallback({ await sendCallback({
taskId: request.taskId, taskId: request.taskId,
@@ -49,35 +72,57 @@ export async function process(
return; return;
} }
// Read and parse torrent file
const torrentData = await readFile(filePath); const torrentData = await readFile(filePath);
const parsed = bencode.decode(torrentData, 'utf8') as Record<string, any>; const parsed = bencode.decode(torrentData, 'utf8') as Record<string, any>;
const metadata: Record<string, string> = {}; const metadata: Record<string, string> = {};
// 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?.(); const announce = parsed.announce?.toString?.();
if (announce) metadata.announce = announce; if (announce) {
metadata.announce = announce;
}
// Torrent name // Extract optional metadata
const name = parsed.info?.name?.toString?.();
if (name) metadata['info/name'] = name;
// Optional metadata
const comment = parsed.comment?.toString?.(); const comment = parsed.comment?.toString?.();
if (comment) metadata.comment = comment; if (comment) {
metadata.comment = comment;
}
const createdBy = parsed['created by']?.toString?.(); const createdBy = parsed['created by']?.toString?.();
if (createdBy) metadata.createdBy = createdBy; if (createdBy) {
metadata.createdBy = createdBy;
}
const creationDate = parsed['creation date']; const creationDate = parsed['creation date'];
if (creationDate != null) { if (creationDate != null) {
metadata.creationDate = String(creationDate); metadata.creationDate = String(creationDate);
} }
// Files info // Extract info section - files
try {
const files = parsed.info?.files; const files = parsed.info?.files;
if (files) {
let i = 0; let i = 0;
for (const key in files) { for (const key in files || {}) {
const file = files[key]; const file = files[key];
const length = file.length; const length = file.length;
const pathParts = file.path; const pathParts = file.path;
@@ -88,13 +133,21 @@ export async function process(
metadata[`info/files/${i}/length`] = String(length); metadata[`info/files/${i}/length`] = String(length);
metadata[`info/files/${i}/path`] = pathStr; metadata[`info/files/${i}/path`] = pathStr;
i++; 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); await metaCore.mergeMetadata(cid, metadata);
// Add announce list to set // Extract announce list (trackers) using add for RecordSet
const announceList = parsed['announce-list'] || []; const announceList = parsed['announce-list'] || [];
for (const tier of announceList) { for (const tier of announceList) {
for (const tracker of tier || []) { for (const tracker of tier || []) {
@@ -104,12 +157,15 @@ export async function process(
} }
} }
console.log(`[torrent] Parsed torrent file: ${filePath}`);
await sendCallback({ await sendCallback({
taskId: request.taskId, taskId: request.taskId,
status: 'completed', status: 'completed',
duration: Date.now() - startTime, duration: Date.now() - startTime,
}); });
} catch (error) { } catch (error) {
console.error(`[torrent] Error parsing torrent file ${request.filePath}:`, error);
await sendCallback({ await sendCallback({
taskId: request.taskId, taskId: request.taskId,
status: 'failed', status: 'failed',