From 991677d1e249e21ffe5cddc05af36012f96c54ee Mon Sep 17 00:00:00 2001 From: Pierre Date: Sat, 6 Jun 2026 15:24:51 +0200 Subject: [PATCH] plugin detection improvement --- .gitignore | 3 +++ Dockerfile | 19 ++++++++++--------- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- src/index.ts | 8 ++++++-- src/plugin.ts | 46 +++++++++++++++++++++++++++++++++++++++++++++- src/types.ts | 3 ++- 7 files changed, 72 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 069c482..67b8f84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ node_modules/ dist/ +# Locally-built @metazla/filename-tools tarball (vendored file: dep; rebuilt from +# packages/filename-tool — see Dockerfile note). Build artifact, not source. +vendor/ *.log .env .DS_Store diff --git a/Dockerfile b/Dockerfile index 13cd0a6..8c340dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,13 +12,16 @@ RUN npm install -g corepack@latest && corepack enable WORKDIR /app -# Copy package files first for layer caching +# Copy package files first for layer caching. vendor/ holds the locally-built +# @metazla/filename-tools tarball (file: dep) — replaces github:worph/filename-tool +# so local lib changes ship without a GitHub push. The tarball is pre-built +# (dist/ included), so no in-place rebuild step is needed. --no-frozen-lockfile +# because the committed lockfile still pins the old github tarball; pnpm +# reconciles it to the file: dep at build time. COPY package.json pnpm-lock.yaml ./ +COPY vendor/ ./vendor/ RUN corepack install -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile - -# Build the git dependency (filename-tools) -RUN cd node_modules/@metazla/filename-tools && pnpm install && pnpm run build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile # Copy source and build COPY tsconfig.json ./ @@ -38,11 +41,9 @@ RUN npm install -g corepack@latest && corepack enable WORKDIR /app COPY package.json pnpm-lock.yaml ./ +COPY vendor/ ./vendor/ RUN corepack install -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --prod - -# Build the git dependency in production too -RUN cd node_modules/@metazla/filename-tools && pnpm install && pnpm run build +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --no-frozen-lockfile --prod COPY --from=builder /app/dist ./dist diff --git a/package.json b/package.json index b1375e0..f73c377 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ }, "dependencies": { "fastify": "^5.0.0", - "@metazla/filename-tools": "github:worph/filename-tool" + "@metazla/filename-tools": "file:vendor/filename-tools.tgz" }, "devDependencies": { "@types/node": "^20.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 63b2946..b375139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@metazla/filename-tools': - specifier: github:worph/filename-tool - version: https://codeload.github.com/worph/filename-tool/tar.gz/8a9c357c8af20bf7af040985993f5f78d1aa5251 + specifier: file:vendor/filename-tools.tgz + version: file:vendor/filename-tools.tgz fastify: specifier: ^5.0.0 version: 5.7.4 @@ -193,8 +193,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@metazla/filename-tools@https://codeload.github.com/worph/filename-tool/tar.gz/8a9c357c8af20bf7af040985993f5f78d1aa5251': - resolution: {tarball: https://codeload.github.com/worph/filename-tool/tar.gz/8a9c357c8af20bf7af040985993f5f78d1aa5251} + '@metazla/filename-tools@file:vendor/filename-tools.tgz': + resolution: {integrity: sha512-S20j5k3Ga2Dn8c2fThKmWsGjPzmb4Ckr/OTFjG5WQX1Ua1ry8X88/Cfql+AYaNGCjTbgIG51T4SbIMgGd285XA==, tarball: file:vendor/filename-tools.tgz} version: 1.0.0 '@pinojs/redact@0.4.0': @@ -953,7 +953,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} - '@metazla/filename-tools@https://codeload.github.com/worph/filename-tool/tar.gz/8a9c357c8af20bf7af040985993f5f78d1aa5251': + '@metazla/filename-tools@file:vendor/filename-tools.tgz': dependencies: file-type: 19.6.0 franc: 6.2.0 diff --git a/src/index.ts b/src/index.ts index 23e8d45..3fc7663 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,8 +35,12 @@ app.post<{ Body: ConfigureRequest }>('/configure', async (request): Promise('/process', async (request, reply) => { - const { taskId, cid, filePath, callbackUrl, metaCoreUrl } = request.body; - if (!taskId || !cid || !filePath || !callbackUrl || !metaCoreUrl) { + // filePath is optional: the parser works on a name string, which may be + // supplied either as filePath or via existingMeta.fileName (the gateway + // feeder path, where no file exists on disk). The required wire fields are + // the write target (cid) + the callback/meta-core plumbing. + const { taskId, cid, callbackUrl, metaCoreUrl } = request.body; + if (!taskId || !cid || !callbackUrl || !metaCoreUrl) { return reply.send({ status: 'rejected', error: 'Missing required fields' } as ProcessResponse); } processFile(request.body, async (payload: CallbackPayload) => { diff --git a/src/plugin.ts b/src/plugin.ts index 07857ba..fbd9291 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -4,6 +4,7 @@ * 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 { @@ -38,6 +39,16 @@ export const manifest: PluginManifest = { 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/` 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 @@ -60,8 +71,22 @@ export async function process( 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(filePath); + const metadata = extractor.extractVideoFileMetadata(nameToParse); // Convert to record for merging const result: Record = {}; @@ -73,6 +98,25 @@ export async function process( 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/ = "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. + if (metadata.videoType === 'tvshow') result.contentKind = 'episode'; + else if (metadata.videoType === 'movie') result.contentKind = 'movie'; + } + if (Object.keys(result).length > 0) { await metaCore.mergeMetadata(cid, result); } diff --git a/src/types.ts b/src/types.ts index 4dbbfe2..5fe7c51 100644 --- a/src/types.ts +++ b/src/types.ts @@ -41,7 +41,8 @@ export interface HealthResponse { export interface ProcessRequest { taskId: string; cid: string; - filePath: string; + /** Optional: falls back to existingMeta.fileName as the name to parse. */ + filePath?: string; callbackUrl: string; metaCoreUrl: string; existingMeta?: Record;