// Wild Dragon — AMPP FramelightX API Client // Handles authenticated requests, folder hierarchy creation. import pool from '../db/pool.js'; /** * Load AMPP credentials from the settings table. * Returns null if not yet configured. */ export async function getAmppConfig() { const result = await pool.query( "SELECT key, value FROM settings WHERE key IN ('ampp_base_url', 'ampp_token')" ); const config = {}; for (const row of result.rows) { config[row.key] = row.value; } if (!config.ampp_base_url || !config.ampp_token) { return null; } return config; } /** * Make an authenticated request to the AMPP REST API. */ async function amppRequest(config, method, path, body = null) { const base = config.ampp_base_url.replace(/\/$/, ''); const url = `${base}/${path.replace(/^\//, '')}`; const opts = { method, headers: { Authorization: `Bearer ${config.ampp_token}`, 'Content-Type': 'application/json', }, }; if (body !== null) { opts.body = typeof body === 'string' ? body : JSON.stringify(body); } const res = await fetch(url, opts); if (!res.ok) { const text = await res.text().catch(() => ''); throw new Error(`AMPP ${method} ${path} → ${res.status}: ${text}`); } return res.json(); } /** * Ensure a nested folder path exists in AMPP, creating any missing segments. * * @param {object} config - Config object from getAmppConfig() * @param {string[]} segments - Ordered path segments, e.g. ['ShowName', 'Videos', 'B-Roll'] * @returns {string|null} The folder:id of the leaf folder, or null if segments is empty. */ export async function ensureFolderPath(config, segments) { let parentId = null; let currentPath = ''; let folderId = null; for (const rawSegment of segments) { const segment = rawSegment.trim(); if (!segment) continue; currentPath = currentPath ? `${currentPath}/${segment}` : segment; const expectedDepth = currentPath.split('/').length; // URL-encode each segment individually, keep slashes as separators const encodedPath = currentPath .split('/') .map((s) => encodeURIComponent(s)) .join('/'); // Check whether this path already exists in AMPP try { const resp = await amppRequest( config, 'GET', `api/v1/store/folder/folders/hierarchy?path=${encodedPath}` ); if (resp && typeof resp === 'object') { const hlist = resp['hierarchy:list']; if (Array.isArray(hlist) && hlist.length === expectedDepth) { const candidate = String(hlist[hlist.length - 1]?.['folder:id'] || '').trim(); if (candidate) { folderId = candidate; parentId = folderId; continue; // Already exists — move to next segment } } } } catch { // Lookup failed — fall through to create } // Create the folder const createBody = { 'name:text': segment }; if (parentId) createBody['parentFolders:tags'] = [parentId]; const cresp = await amppRequest( config, 'POST', 'api/v1/store/folder/folders', createBody ); folderId = String(cresp?.['folder:id'] || '').trim(); if (!folderId) { throw new Error( `No folder:id in create response for segment "${segment}" (path: ${currentPath})` ); } parentId = folderId; } return folderId || null; }