diff --git a/services/mam-api/src/ampp/client.js b/services/mam-api/src/ampp/client.js new file mode 100644 index 0000000..202ec5c --- /dev/null +++ b/services/mam-api/src/ampp/client.js @@ -0,0 +1,115 @@ +// 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; +}