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 3ad4b4c..22c9a8f 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", "wanakana": "^5.3.1" }, + "dependencies": { + "fastify": "^5.0.0", + "wanakana": "^5.3.1", + "@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..e734fdc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,26 @@ +/** + * MetaMesh Plugin: anime-detector + * + * ============================================================================ + * 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 (anime-detector) only requires mount 2: + * - /cache (RW) - to cache detection results + * - Note: This plugin analyzes metadata, doesn't read files directly + * + * 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 d4db0a4..b4e9574 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,12 +1,27 @@ /** * Anime Detector Plugin - * Detects if content is anime based on keywords and Japanese text + * + * Detects if a file is likely anime based on: + * - Common anime keywords in filename + * - Japanese text in title (kana, kanji) + * - Japanese audio tracks + * - "anime" in file path + * + * Matches old AnimeDetector output: + * - titles/jpn (if kana) + * - titles/{romajiIsoCode} (if romaji) + * - anime + * - genres (add "Anime") */ import { isJapanese, isKana } from 'wanakana'; +import { animeKeywords } from '@metazla/filename-tools'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import { MetaCoreClient } from './meta-core-client.js'; +// romajiIsoCode from @metazla/meta-interface is just 'jpl' +const romajiIsoCode = 'jpl'; + export const manifest: PluginManifest = { id: 'anime-detector', name: 'Anime Detector', @@ -21,41 +36,11 @@ export const manifest: PluginManifest = { schema: { anime: { label: 'Is Anime', type: 'boolean', readonly: true }, 'titles/jpn': { label: 'Japanese Title', type: 'string' }, + [`titles/${romajiIsoCode}`]: { label: 'Romaji Title', type: 'string' }, }, config: {}, }; -// Anime keywords from @metazla/filename-tools - fansub group identifiers only -// Note: Codec patterns (HEVC, x265, etc.) are NOT anime indicators -const ANIME_KEYWORDS = [ - '[HorribleSubs]', - '[DB]', // Dragon Ball fansubs, also generic for various sub groups - '[Erai-raws]', - '[SubsPlease]', - '[GG]', // A known fansub group - '[Commie]', // A fansubbing group - '[Underwater]', // A fansubbing group - '[Nyaa]', // A reference to the Nyaa Torrents site, often included in anime torrents - '[FFF]', // A fansubbing group - '[Coalgirls]', // A fansubbing group known for high-quality releases - '[Chihiro]', // A fansubbing group - '[DameDesuYo]', // A fansubbing group - '[GJM]', // A fansubbing group (Good Job! Media) - '[AhorribleSubs]', // A misspelling of HorribleSubs sometimes used - '[AnimeRG]', // A group known for releasing anime torrents - '[Judas]', // A group known for their anime releases - '[Cleo]', // A group known for their anime encodes - '[LostYears]', // A group known for dub and sub releases - '[KaiDubs]', // A group known for dubbed anime releases - '[AnimeKaizoku]', // A website known for anime downloads, sometimes included in filenames - '[AnimeLand]', // A website known for anime downloads, sometimes included in filenames - '[Beatrice-Raws]', // A group known for raw anime blu-ray encodes - '[BakedFish]', // A less common but recognized release group - '[DeadFish]', // A group known for re-encoding fansubs into hardsubbed mp4 format - '[Golumpa]', // A group known for dubbed anime releases - '[EMBER]', // A group known for their anime releases -]; - export async function process( request: ProcessRequest, sendCallback: (payload: CallbackPayload) => Promise @@ -79,6 +64,7 @@ export async function process( const originalTitle = existingMeta?.originalTitle || ''; const fileName = existingMeta?.fileName || ''; + let isAnime = false; let isJpn = false; @@ -88,8 +74,8 @@ export async function process( isJpn = true; } - // Check for anime keywords - for (const keyword of ANIME_KEYWORDS) { + // Check for anime keywords in filename (from @metazla/filename-tools) + for (const keyword of animeKeywords) { if (fileName.includes(keyword)) { isAnime = true; isJpn = true; @@ -97,45 +83,46 @@ export async function process( } } - // Check if title is Japanese + // Check if title is in Japanese if (originalTitle && (isKana(originalTitle) || isJapanese(originalTitle))) { isJpn = true; } // Check audio streams for Japanese language - for (let i = 0; i < 10; i++) { + for (let i = 0; i < 20; i++) { const lang = existingMeta?.[`fileinfo/streamdetails/audio/${i}/language`]; - if (lang === 'jpn' || lang === 'ja') { + if (!lang) break; + if (lang === 'jpn') { isJpn = true; break; } - if (!lang) break; } - // Check video streams for Japanese language (matching original) - for (let i = 0; i < 10; i++) { + // Check video streams for Japanese language + for (let i = 0; i < 20; i++) { const lang = existingMeta?.[`fileinfo/streamdetails/video/${i}/language`]; - if (lang === 'jpn' || lang === 'ja') { + if (!lang) break; + if (lang === 'jpn') { isJpn = true; break; } - if (!lang) break; } - const metadata: Record = {}; - + // Set Japanese title appropriately if (isJpn && originalTitle) { if (isKana(originalTitle)) { - metadata['titles/jpn'] = originalTitle; + await metaCore.setProperty(cid, 'titles/jpn', originalTitle); } else { - metadata['titles/rom'] = originalTitle; + await metaCore.setProperty(cid, `titles/${romajiIsoCode}`, originalTitle); } } + // Anything Japanese is considered anime (as per original implementation) if (isAnime || isJpn) { - metadata.anime = 'true'; - await metaCore.mergeMetadata(cid, metadata); + await metaCore.setProperty(cid, 'anime', 'true'); + // Use add for genres (RecordSet) await metaCore.addToSet(cid, 'genres', 'Anime'); + console.log(`[anime-detector] Detected anime: ${fileName}`); } await sendCallback({