diff --git a/src/plugin.ts b/src/plugin.ts index 26744ab..86f4402 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -18,6 +18,15 @@ import { franc } from 'franc-min'; import { anyTo_iso_639_3 } from '@metazla/filename-tools'; import type { PluginManifest, ProcessRequest, CallbackPayload } from './types.js'; import { MetaCoreClient } from './meta-core-client.js'; +import { createWebDAVClient, WebDAVClient } from './webdav-client.js'; + +// Initialize WebDAV client if WEBDAV_URL is set +const webdavClient = createWebDAVClient(); +if (webdavClient) { + console.log('[jellyfin-nfo] Using WebDAV for file access'); +} else { + console.log('[jellyfin-nfo] Using direct filesystem access'); +} const englishIsoCode = 'eng'; @@ -49,6 +58,9 @@ export const manifest: PluginManifest = { }; async function fileExists(path: string): Promise { + if (webdavClient) { + return webdavClient.exists(path); + } try { await access(path); return true; @@ -57,8 +69,15 @@ async function fileExists(path: string): Promise { } } +async function readNfoContent(nfoPath: string): Promise { + if (webdavClient) { + return webdavClient.readText(nfoPath, 'utf8'); + } + return readFile(nfoPath, 'utf8'); +} + async function parseNfoFile(nfoPath: string): Promise { - const content = await readFile(nfoPath, 'utf8'); + const content = await readNfoContent(nfoPath); const parser = new Parser({ explicitArray: false, mergeAttrs: true }); return parser.parseStringPromise(content); } @@ -95,7 +114,7 @@ export async function process( const nfoPath = filePath.replace(/\.[^.]+$/, '.nfo'); if (await fileExists(nfoPath)) { try { - const nfoContent = await readFile(nfoPath, 'utf8'); + const nfoContent = await readNfoContent(nfoPath); const parser = new Parser({ explicitArray: false, mergeAttrs: true }); const parsed = await parser.parseStringPromise(nfoContent); await extractNfoData(metaCore, cid, parsed, filePath); @@ -109,7 +128,7 @@ export async function process( const tvShowNfoPath = join(dirPath, 'tvshow.nfo'); if (await fileExists(tvShowNfoPath)) { try { - const nfoContent = await readFile(tvShowNfoPath, 'utf8'); + const nfoContent = await readNfoContent(tvShowNfoPath); const parser = new Parser({ explicitArray: false, mergeAttrs: true }); const parsed = await parser.parseStringPromise(nfoContent); await extractNfoData(metaCore, cid, parsed, filePath); @@ -118,7 +137,8 @@ export async function process( } } - console.log(`[jellyfin-nfo] Processed NFO for ${filePath}`); + const mode = webdavClient ? 'WebDAV' : 'filesystem'; + console.log(`[jellyfin-nfo] Processed NFO for ${filePath} (${mode})`); await sendCallback({ taskId: request.taskId, diff --git a/src/webdav-client.ts b/src/webdav-client.ts new file mode 100644 index 0000000..e72bd28 --- /dev/null +++ b/src/webdav-client.ts @@ -0,0 +1,117 @@ +/** + * WebDAV Client for Plugin File Access + * + * Provides HTTP-based file access to meta-sort's WebDAV endpoint. + * Replaces direct filesystem access for containerized plugins. + */ + +export interface FileStats { + size: number; + mtime?: Date; +} + +export class WebDAVClient { + private baseUrl: string; + + constructor(baseUrl: string) { + // Remove trailing slash if present + this.baseUrl = baseUrl.replace(/\/$/, ''); + } + + /** + * Convert absolute file path to WebDAV URL + * Strips /files prefix and appends to base URL + */ + toWebDAVUrl(filePath: string): string { + // filePath is like "/files/watch/movie.mkv" + // We need to convert to "http://meta-sort-dev/webdav/watch/movie.mkv" + let relativePath = filePath; + + // Strip /files prefix if present + if (relativePath.startsWith('/files/')) { + relativePath = relativePath.substring(6); // Remove "/files" + } else if (relativePath.startsWith('/files')) { + relativePath = relativePath.substring(6); + } + + // Ensure path starts with / + if (!relativePath.startsWith('/')) { + relativePath = '/' + relativePath; + } + + return this.baseUrl + relativePath; + } + + /** + * Get file stats (size, mtime) via HTTP HEAD request + */ + async stat(filePath: string): Promise { + const url = this.toWebDAVUrl(filePath); + + const response = await fetch(url, { method: 'HEAD' }); + + if (!response.ok) { + throw new Error(`WebDAV HEAD failed for ${filePath}: ${response.status} ${response.statusText}`); + } + + const contentLength = response.headers.get('content-length'); + const lastModified = response.headers.get('last-modified'); + + return { + size: contentLength ? parseInt(contentLength, 10) : 0, + mtime: lastModified ? new Date(lastModified) : undefined, + }; + } + + /** + * Read entire file as buffer (for small files like NFO) + */ + async readFile(filePath: string): Promise { + const url = this.toWebDAVUrl(filePath); + + const response = await fetch(url, { method: 'GET' }); + + if (!response.ok) { + throw new Error(`WebDAV GET failed for ${filePath}: ${response.status} ${response.statusText}`); + } + + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); + } + + /** + * Read file as text (for NFO XML files) + */ + async readText(filePath: string, encoding: BufferEncoding = 'utf8'): Promise { + const buffer = await this.readFile(filePath); + return buffer.toString(encoding); + } + + /** + * Check if file exists via HTTP HEAD + */ + async exists(filePath: string): Promise { + const url = this.toWebDAVUrl(filePath); + + try { + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } catch { + return false; + } + } +} + +/** + * Create a WebDAV client from environment variables + */ +export function createWebDAVClient(): WebDAVClient | null { + const webdavUrl = process.env.WEBDAV_URL; + + if (!webdavUrl) { + console.warn('[webdav-client] WEBDAV_URL not set, WebDAV client unavailable'); + return null; + } + + return new WebDAVClient(webdavUrl); +}