bugfix
This commit is contained in:
@@ -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
@@ -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" }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
+72
-16
@@ -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
|
||||||
const files = parsed.info?.files;
|
try {
|
||||||
if (files) {
|
const files = parsed.info?.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',
|
||||||
|
|||||||
Reference in New Issue
Block a user