This commit is contained in:
Pierre
2026-01-10 19:14:39 +01:00
parent 96585b495d
commit 0e0b6d8d53
4 changed files with 69 additions and 49 deletions
+6
View File
@@ -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 \
+5 -1
View File
@@ -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" }
}
+23
View File
@@ -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/<id> (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/<id> only.
*
* ============================================================================
*/
import Fastify from 'fastify';
import type { HealthResponse, ProcessRequest, ProcessResponse, CallbackPayload, ConfigureRequest, ConfigureResponse } from './types.js';
import { manifest, process as processFile } from './plugin.js';
+35 -48
View File
@@ -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<void>
@@ -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<string, string> = {};
// 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({