Files
metamesh-plugin-filename-pa…/src/plugin.ts
T
2026-10-25 17:36:44 +00:00

153 lines
6.2 KiB
TypeScript

/**
* Filename Parser Plugin
* Parses video filenames to extract title, season, episode, year
* Uses @metazla/filename-tools library for parsing
*/
import { env } from 'node:process';
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
import { MetaCoreClient } from './meta-core-client.js';
import {
FileNameVideoMetaExtractor,
episodePatterns,
seasonAndEpisodePatterns,
seasonPatterns,
extraEpKeyWords,
keywordsArray,
substringArray,
soloEp
} from '@metazla/filename-tools';
export const manifest: PluginManifest = {
id: 'filename-parser',
name: 'Filename Parser',
version: '1.0.0',
description: 'Parses video filenames to extract title, season, episode, year, and quality',
author: 'MetaMesh',
dependencies: ['file-info'],
priority: 20,
color: '#2196F3',
defaultQueue: 'fast',
timeout: 30000,
schema: {
parsedTitle: { label: 'Parsed Title', type: 'string', hint: 'title guessed from the filename; never written to title/originalTitle' },
videoType: { label: 'Video Type', type: 'string', hint: 'movie or tvshow' },
season: { label: 'Season', type: 'string' },
episode: { label: 'Episode', type: 'string' },
movieYear: { label: 'Year', type: 'number' },
},
config: {},
};
/**
* Emit the title-derived release tags (quality / codec / languages) added in the
* filename-tools lib. **Opt-in (default off)** because these are title *guesses*:
* in meta-sort the authoritative values come from probing the real stream
* (ffmpeg / language plugin), and a title-guessed `languages/<iso>` set member
* would pollute that. Gateway feeders (no real file) enable it via
* `EMIT_TITLE_TAGS=true`; meta-sort leaves it off. (`real-probe > title-guess`.)
*/
const EMIT_TITLE_TAGS = /^(1|true|yes)$/i.test(env.EMIT_TITLE_TAGS ?? '');
// Create extractor instance with default config
const extractor = new FileNameVideoMetaExtractor(
[], // watchFolderList - empty for plugin context
episodePatterns,
seasonAndEpisodePatterns,
seasonPatterns,
extraEpKeyWords,
keywordsArray,
substringArray,
soloEp
);
export async function process(
request: ProcessRequest,
sendCallback: (payload: CallbackPayload) => Promise<void>
): Promise<void> {
const startTime = Date.now();
const metaCore = new MetaCoreClient(request.metaCoreUrl);
try {
const { cid, filePath } = request;
// The parser operates on a name string. Accept it via filePath or, when
// the caller has no file on disk (gateway feeder path), via
// existingMeta.fileName. Skip cleanly if neither is present.
const nameToParse = filePath || request.existingMeta?.fileName;
if (!nameToParse) {
await sendCallback({
taskId: request.taskId,
status: 'skipped',
duration: Date.now() - startTime,
reason: 'No filePath or existingMeta.fileName to parse',
});
return;
}
// Use the library to extract metadata
const metadata = extractor.extractVideoFileMetadata(nameToParse);
// Convert to record for merging
const result: Record<string, string> = {};
// A filename guess is a different fact from a catalogue title, so it gets
// its own key (METADATA_KEYS.md rule #8): written to `originalTitle` it
// would compete with tmdb as a separate signer and, being older, win.
if (metadata.originalTitle) result.parsedTitle = metadata.originalTitle;
if (metadata.season) result.season = metadata.season;
if (metadata.episode) result.episode = metadata.episode;
if (metadata.increment) result.increment = metadata.increment;
if (metadata.movieYear) result.movieYear = metadata.movieYear;
if (metadata.videoType) result.videoType = metadata.videoType;
if (metadata.extra) result.extra = metadata.extra;
// Title-derived release tags — opt-in (see EMIT_TITLE_TAGS). quality /
// codec are scalars; languages is a `languages/<iso> = "true"` key-set
// (METADATA_KEYS §1), the same shape gateway feeders + meta-watch use.
if (EMIT_TITLE_TAGS) {
if (metadata.quality) result.quality = metadata.quality;
if (metadata.codec) result.codec = metadata.codec;
for (const lang of metadata.languages ?? []) {
result[`languages/${lang}`] = 'true';
}
// contentKind mirrors videoType (movie→movie, tvshow→episode).
// Gateway feeders retire their Rust filename port and rely on this
// plugin as the sole filename source, so it owns this derivation
// too (meta-watch reads contentKind to detect/aggregate episodes).
// Gated with the other title-derived tags so meta-sort (the real
// file is the authority there, flag off) is unaffected.
// `domain` travels with `contentKind` — whoever writes one writes
// the other (METADATA_KEYS.md §1). It is what every client filters
// its wall on, and a record with a kind but no domain is invisible
// to all of them. Stamped literally rather than through
// filename-tool's `domainForContentKind` because this plugin
// vendors that package as a tarball; the two kinds here are the
// whole table's video half anyway.
if (metadata.videoType === 'tvshow') {
result.contentKind = 'episode';
result.domain = 'tv';
} else if (metadata.videoType === 'movie') {
result.contentKind = 'movie';
result.domain = 'film';
}
}
if (Object.keys(result).length > 0) {
await metaCore.mergeMetadata(cid, result);
}
await sendCallback({
taskId: request.taskId,
status: 'completed',
duration: Date.now() - startTime,
});
} catch (error) {
await sendCallback({
taskId: request.taskId,
status: 'failed',
duration: Date.now() - startTime,
error: error instanceof Error ? error.message : String(error),
});
}
}