* TOML utility functions for configuration export
*/
* Flattens nested configuration object and generates environment variable names
* @param {Object} config - The nested configuration object from the backend
* @param {string} prefix - The current prefix for nested keys
* @returns {Array} - Array of config objects with key, envVar, and value properties
*/
export const flattenConfig = (config, prefix = '') => {
const result = []
if (!config || typeof config !== 'object') {
return result
}
Object.keys(config).forEach((key) => {
const value = config[key]
const currentKey = prefix ? `${prefix}.${key}` : key
if (value && typeof value === 'object' && !Array.isArray(value)) {
result.push(...flattenConfig(value, currentKey))
} else {
const envVar = 'ND_' + currentKey.toUpperCase().replace(/\./g, '_')
let displayValue = value
if (
Array.isArray(value) ||
(typeof value === 'object' && value !== null)
) {
displayValue = JSON.stringify(value)
} else {
displayValue = String(value)
}
result.push({
key: currentKey,
envVar: envVar,
value: displayValue,
})
}
})
return result
}
* Separates and sorts configuration entries into regular and dev configs
* @param {Array|Object} configEntries - Array of config objects with key and value, or nested config object
* @returns {Object} - Object with regularConfigs and devConfigs arrays, both sorted
*/
export const separateAndSortConfigs = (configEntries) => {
const regularConfigs = []
const devConfigs = []
let flattenedConfigs
if (Array.isArray(configEntries)) {
flattenedConfigs = configEntries
} else {
flattenedConfigs = flattenConfig(configEntries)
}
flattenedConfigs?.forEach((config) => {
if (config.key === 'ConfigFile') {
return
}
if (config.key.startsWith('Dev')) {
devConfigs.push(config)
} else {
regularConfigs.push(config)
}
})
regularConfigs.sort((a, b) => a.key.localeCompare(b.key))
devConfigs.sort((a, b) => a.key.localeCompare(b.key))
return { regularConfigs, devConfigs }
}
* Escapes TOML keys that contain special characters
* @param {string} key - The key to potentially escape
* @returns {string} - The escaped key if needed, or the original key
*/
export const escapeTomlKey = (key) => {
const keyStr = String(key)
if (keyStr === '') {
return '""'
}
if (/^[a-zA-Z0-9_-]+$/.test(keyStr)) {
return keyStr
}
return `"${keyStr.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
}
* Converts a value to proper TOML format
* @param {*} value - The value to format
* @returns {string} - The TOML-formatted value
*/
export const formatTomlValue = (value) => {
if (value === null || value === undefined) {
return '""'
}
const str = String(value)
if (str === 'true' || str === 'false') {
return str
}
if (/^-?\d+$/.test(str)) {
return str
}
if (/^-?\d*\.\d+$/.test(str)) {
return str
}
if (/^\d+(\.\d+)?(ns|us|ยตs|ms|s|m|h)$/.test(str)) {
return `"${str}"`
}
if (str.startsWith('[') || str.startsWith('{')) {
try {
const parsed = JSON.parse(str)
if (Array.isArray(parsed)) {
const formattedItems = parsed.map((item) => {
if (typeof item === 'string') {
return `"${item.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
} else if (typeof item === 'number' || typeof item === 'boolean') {
return String(item)
} else {
return `"${String(item).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
}
})
if (formattedItems.length === 0) {
return '[ ]'
}
return `[ ${formattedItems.join(', ')} ]`
}
return `"""${str}"""`
} catch {
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
}
}
return `"${str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
}
* Converts nested keys to TOML sections
* @param {Array} configs - Array of config objects with key and value
* @returns {Object} - Object with sections and rootKeys
*/
export const buildTomlSections = (configs) => {
const sections = {}
const rootKeys = []
configs.forEach(({ key, value }) => {
if (key.includes('.')) {
const parts = key.split('.')
const sectionName = parts[0]
const keyName = parts.slice(1).join('.')
if (!sections[sectionName]) {
sections[sectionName] = []
}
sections[sectionName].push({ key: keyName, value })
} else {
rootKeys.push({ key, value })
}
})
return { sections, rootKeys }
}
* Converts configuration data to TOML format
* @param {Object} configData - The configuration data object
* @param {Function} translate - Translation function for internationalization
* @returns {string} - The TOML-formatted configuration
*/
export const configToToml = (configData, translate = (key) => key) => {
let tomlContent = `# Navidrome Configuration\n# Generated on ${new Date().toISOString()}\n\n`
let configs
if (Array.isArray(configData.config)) {
configs = configData.config
} else {
configs = flattenConfig(configData.config)
}
const { regularConfigs, devConfigs } = separateAndSortConfigs(configs)
const { sections: regularSections, rootKeys: regularRootKeys } =
buildTomlSections(regularConfigs)
if (regularRootKeys.length > 0) {
regularRootKeys.forEach(({ key, value }) => {
tomlContent += `${key} = ${formatTomlValue(value)}\n`
})
tomlContent += '\n'
}
if (devConfigs.length > 0) {
tomlContent += `# ${translate('about.config.devFlagsHeader')}\n`
tomlContent += `# ${translate('about.config.devFlagsComment')}\n\n`
const { sections: devSections, rootKeys: devRootKeys } =
buildTomlSections(devConfigs)
devRootKeys.forEach(({ key, value }) => {
tomlContent += `${key} = ${formatTomlValue(value)}\n`
})
if (devRootKeys.length > 0) {
tomlContent += '\n'
}
Object.keys(devSections)
.sort()
.forEach((sectionName) => {
tomlContent += `[${sectionName}]\n`
devSections[sectionName].forEach(({ key, value }) => {
tomlContent += `${escapeTomlKey(key)} = ${formatTomlValue(value)}\n`
})
tomlContent += '\n'
})
}
Object.keys(regularSections)
.sort()
.forEach((sectionName) => {
tomlContent += `[${sectionName}]\n`
regularSections[sectionName].forEach(({ key, value }) => {
tomlContent += `${escapeTomlKey(key)} = ${formatTomlValue(value)}\n`
})
tomlContent += '\n'
})
return tomlContent
}