dragonflight/services/mam-api/src/ampp/client.js

115 lines
3.3 KiB
JavaScript

// 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;
}