fix plugin poster

This commit is contained in:
Pierre
2026-01-11 00:25:36 +01:00
parent 1f92bddaa0
commit c5b9561c0c
+94 -32
View File
@@ -30,7 +30,8 @@
*/ */
import axios from 'axios'; import axios from 'axios';
import { createWriteStream, existsSync, mkdirSync } from 'fs'; import { createWriteStream, existsSync, mkdirSync, statSync, openSync, readSync, closeSync } from 'fs';
import { createHash } from 'crypto';
import * as path from 'path'; import * as path from 'path';
import { pipeline } from 'stream/promises'; import { pipeline } from 'stream/promises';
import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js';
@@ -52,6 +53,79 @@ const IMAGE_BASE_URL = 'https://image.tmdb.org/t/p/original';
const FILES_PATH = (globalThis as any).process?.env?.FILES_PATH || '/files'; const FILES_PATH = (globalThis as any).process?.env?.FILES_PATH || '/files';
const PLUGIN_OUTPUT_PATH = '/output'; const PLUGIN_OUTPUT_PATH = '/output';
/**
* Compute midhash256 CID for a file (matches meta-hash algorithm)
*
* Algorithm:
* - For files <= 1MB: Hashes entire file content + 8-byte size prefix
* - For files > 1MB: Hashes middle 1MB + 8-byte size prefix
* - Returns CIDv1 with custom codec 0x1000
*/
function computeMidHash256(filePath: string): string {
const SAMPLE_SIZE = 1024 * 1024; // 1MB
// varint encoding of 0x1000 (4096) for both codec and hash function code
const MIDHASH_VARINT = Buffer.from([0x80, 0x20]);
// Get file size
const stats = statSync(filePath);
const fileSize = stats.size;
// Create size buffer (64-bit big-endian)
const sizeBuffer = Buffer.allocUnsafe(8);
sizeBuffer.writeBigUInt64BE(BigInt(fileSize), 0);
// Extract sample data
let sampleData: Buffer;
if (fileSize <= SAMPLE_SIZE) {
// Small file: read entire content
const fd = openSync(filePath, 'r');
sampleData = Buffer.allocUnsafe(fileSize);
readSync(fd, sampleData, 0, fileSize, 0);
closeSync(fd);
} else {
// Large file: read middle 1MB
const middleOffset = Math.floor((fileSize - SAMPLE_SIZE) / 2);
const fd = openSync(filePath, 'r');
sampleData = Buffer.allocUnsafe(SAMPLE_SIZE);
readSync(fd, sampleData, 0, SAMPLE_SIZE, middleOffset);
closeSync(fd);
}
// Compute SHA-256 hash of [size + sample]
const hashInput = Buffer.concat([sizeBuffer, sampleData]);
const hashBuffer = createHash('sha256').update(hashInput).digest();
// Build CIDv1: version (0x01) + codec (varint) + multihash
// Codec 0x1000 = varint [0x80, 0x20]
// Multihash: function-code (varint 0x1000) + length (0x20) + hash
const cidBytes = Buffer.concat([
Buffer.from([0x01]), // CIDv1
MIDHASH_VARINT, // codec 0x1000 as varint
MIDHASH_VARINT, // hash function code 0x1000 as varint
Buffer.from([0x20]), // 32 bytes digest length
hashBuffer
]);
// Encode as base32lower with 'b' prefix (multibase)
const base32Chars = 'abcdefghijklmnopqrstuvwxyz234567';
let cid = 'b';
let bits = 0;
let value = 0;
for (const byte of cidBytes) {
value = (value << 8) | byte;
bits += 8;
while (bits >= 5) {
bits -= 5;
cid += base32Chars[(value >> bits) & 0x1f];
}
}
if (bits > 0) {
cid += base32Chars[(value << (5 - bits)) & 0x1f];
}
return cid;
}
export const manifest: PluginManifest = { export const manifest: PluginManifest = {
id: 'tmdb', id: 'tmdb',
name: 'TMDB Metadata', name: 'TMDB Metadata',
@@ -217,22 +291,21 @@ async function downloadImage(imageUrl: string, localPath: string): Promise<boole
} }
/** /**
* Download and hash an image, returning its CID and path * Download and hash an image, returning its CID
* *
* IMPORTANT: Images are saved to PLUGIN_OUTPUT_PATH (/output) * IMPORTANT: Images are saved to PLUGIN_OUTPUT_PATH (/output)
* This is the plugin's dedicated output folder with READ-WRITE access. * This is the plugin's dedicated output folder with READ-WRITE access.
* Other plugins can read these files but cannot write to this location. * The files will be picked up by meta-sort's file watcher and processed like any other file.
* *
* @returns {Promise<{cid: string, path: string} | null>} * @returns {Promise<string | null>} The midhash256 CID of the downloaded image
*/ */
async function downloadAndHashImage( async function downloadAndHashImage(
imagePath: string, imagePath: string,
imageType: string, imageType: string,
title: string, title: string,
year: string | undefined, year: string | undefined,
tmdbId: string, tmdbId: string
metaCore: MetaCoreClient ): Promise<string | null> {
): Promise<{ cid: string; path: string } | null> {
if (!imagePath) { if (!imagePath) {
return null; return null;
} }
@@ -250,10 +323,6 @@ async function downloadAndHashImage(
// This respects the mount architecture: /files is READ-ONLY // This respects the mount architecture: /files is READ-ONLY
const localPath = path.join(PLUGIN_OUTPUT_PATH, filename); const localPath = path.join(PLUGIN_OUTPUT_PATH, filename);
// Relative path for storage (relative to /files for meta-core compatibility)
// Path format: plugin/tmdb/<filename>
const relativePath = `plugin/tmdb/${filename}`;
// Check if file already exists (skip download) // Check if file already exists (skip download)
if (existsSync(localPath)) { if (existsSync(localPath)) {
console.log(`[tmdb] Image already exists: ${localPath}`); console.log(`[tmdb] Image already exists: ${localPath}`);
@@ -271,15 +340,11 @@ async function downloadAndHashImage(
} }
} }
// Compute CID using meta-core API // Compute midhash256 CID (same algorithm as meta-sort file entries)
try { try {
const cid = await metaCore.computeFileCID(relativePath); const cid = computeMidHash256(localPath);
if (cid) {
console.log(`[tmdb] Image ${imageType} CID: ${cid}`); console.log(`[tmdb] Image ${imageType} CID: ${cid}`);
return { cid, path: relativePath }; return cid;
} else {
console.warn(`[tmdb] Failed to compute CID for ${localPath}`);
}
} catch (e) { } catch (e) {
console.error(`[tmdb] Failed to compute CID for ${localPath}: ${e instanceof Error ? e.message : String(e)}`); console.error(`[tmdb] Failed to compute CID for ${localPath}: ${e instanceof Error ? e.message : String(e)}`);
} }
@@ -479,35 +544,32 @@ async function applyTmdbData(
await metaCore.addToSet(cid, 'tags', 'tmdb-verified'); await metaCore.addToSet(cid, 'tags', 'tmdb-verified');
// Download poster and backdrop images to plugin output folder // Download poster and backdrop images to plugin output folder
// The files will be picked up by meta-sort's file watcher and processed like any other file
if (data.poster_path && tmdbId) { if (data.poster_path && tmdbId) {
const posterResult = await downloadAndHashImage( const posterCid = await downloadAndHashImage(
data.poster_path, data.poster_path,
'poster', 'poster',
displayTitle, displayTitle,
year, year,
tmdbId, tmdbId
metaCore as any
); );
if (posterResult) { if (posterCid) {
await metaCore.setProperty(cid, 'poster', posterResult.cid); await metaCore.setProperty(cid, 'poster', posterCid);
await metaCore.setProperty(cid, 'posterPath', posterResult.path); console.log(`[tmdb] Set poster CID: ${posterCid}`);
console.log(`[tmdb] Set poster CID: ${posterResult.cid}`);
} }
} }
if (data.backdrop_path && tmdbId) { if (data.backdrop_path && tmdbId) {
const backdropResult = await downloadAndHashImage( const backdropCid = await downloadAndHashImage(
data.backdrop_path, data.backdrop_path,
'backdrop', 'backdrop',
displayTitle, displayTitle,
year, year,
tmdbId, tmdbId
metaCore as any
); );
if (backdropResult) { if (backdropCid) {
await metaCore.setProperty(cid, 'backdrop', backdropResult.cid); await metaCore.setProperty(cid, 'backdrop', backdropCid);
await metaCore.setProperty(cid, 'backdropPath', backdropResult.path); console.log(`[tmdb] Set backdrop CID: ${backdropCid}`);
console.log(`[tmdb] Set backdrop CID: ${backdropResult.cid}`);
} }
} }
} }