import fs from 'fs/promises' import path from 'path' import { exec } from 'child_process' // WAVEFORM GENERATION // TODO@mx use node-js library async function generateWaveform(audioFile, outputDir) { const inputFile = path.join(outputDir, audioFile) audioFile = audioFile.replace(/\.[^/.]+$/, '.png') const outputPath = path.join(outputDir || '.', audioFile) return new Promise((resolve, reject) => { exec( `audiowaveform -i '${inputFile}' -o '${outputPath}' --output-format png -z auto -w 640 -h 180 --background-color 00000000 --waveform-color FFFFFF50 --waveform-style bars --bar-style rounded --border-color 00000000 --bar-width 4 --bar-gap 6 --no-axis-labels`, (error, stdOut) => { if (error) { console.error(error) reject(error) } else { resolve(audioFile) } }) }) } async function generateThumb(imgPath, outputDir) { const { name, ext } = path.parse(imgPath) if (!ext) { return 0 } const thumb = path.format({ name: `${name}.thumb`, ext }) const inputFile = path.join(outputDir, imgPath) const outputFile = path.join(outputDir, thumb) return new Promise((resolve, reject) => { exec( `magick "${inputFile}" -resize 250x250\\> "${outputFile}"`, (error, stdout) => { if (error) { console.error(error) reject(error) } else { resolve(thumb) } } ) }) } // MAIN FUNCTION async function main() { // variable for incoming variable let mediaDir = '' let outputDir = '' let recurse = false let imagesOnly = false let dryRun = false let formatted = null let waveform = false let order = false if (process.argv.length < 3 || process.argv.includes('-h')) { console.log(` Files to Json converter Commands : -h : This message -dir : The input directory to scan -o : The output directory of the json file. If not set will default to current working directory -u : [NOT IMPLEMENTED] List of comma separated feilds to update. Will only overwrite that which is set -r : Recurse the directory (once) for sub-fields. Albums and image categories for example. Other flags --images-only : For a folder with only images, rather than media files with associated thumbs. --dry-run : Output to the console --formatted : Directory or file name, will format on the the undersore : "title_details". This takes the arguments, parent, recurse or both. Defaults to both. --gen-waveform : [may break if recursing] If going through audio files can generate waveform images for the file. --order : Take number & number + "." as order of array `) return } // get command line arguments process.argv.forEach((val, index) => { switch (val) { case '-dir': mediaDir = process.argv[index + 1] || '' break case '-o': outputDir = process.argv[index + 1] || '' break case '-u': update = process.argv[index + 1].match(/(title|details|media|image[s]*|tracks|)/i) ? process.argv[index + 1] .split(',') .filter(a => [ 'title', 'details', 'media', 'image', 'tracks', 'images' ].includes(a)) : true break case '-r': recurse = true break case '--images-only': imagesOnly = true break case '--dry-run': dryRun = true break case '--formatted': formatted = [ 'parent', 'recurse', 'both' ].includes( process.argv[index + 1] ) ? process.argv[index + 1] : 'both' break case '--gen-waveform': waveform = true break case '--order': order = true break default: break } }) // if no media dir passed jump out if (!mediaDir) { console.log('no directory passed to script') return 0 } // // read directory const dirList = await fs.readdir(mediaDir) // variable to hold media let media = [] let images = [] // acutal media to playback or see if (recurse) { media = dirList.filter(i => !i.match(/\.[^/.]{1,4}$/)) } else { media = dirList.filter(i => i.match(/.(mp\d|m\d\w)$/i)) } // create an array of images found in the directory as well images = dirList.filter(i => i.match(/.(jp\w*g|png)$/i)) // clean up passed directory for json let parent_dir = mediaDir .match(/(?<=\/)(\w|\d)+\/*$/i)[0] .replace(/\/$/i, '') // set an output directory if not set by flags if (!outputDir) { outputDir = '.' } // THE MAIN EVENT const obj = await Promise.all(media.map(async m => { // setup structure const _r = { ...genMetadata(m, formatted == 'parent' || formatted == 'both', order), parent_dir, media: m, image: '', order: 0 } if (order) { const idx = m.match(/\d+/) ? m.match(/\d+/)[0] : 0 _r.order = idx _r.title = _r.title.replace(/^\d+\W*_*/, '') } // if we are scanning for images only, remove the 'image' property // as it will be saved in the media property if (imagesOnly) { delete _r.image } else { if (waveform) { // if waveform generate waveform _r.image = await generateWaveform(m, mediaDir) } else { // if not compare the media name with the image name // and if a match assign it to 'image' variable from earlier _r.image = linkImage(m.replace(/\.[^/.]+$/, ''), images) } } // if recurse (again) now we are looking for the media in the folders if (recurse) { // create full path from the passed dir, and the recuring dir const rDir = path.join(mediaDir, m) // read the dir let files = await fs.readdir(rDir) // filterout any rogue folders // generate the metadata for each file files = files.filter(f => f.match(/(? { const { title, details } = genMetadata(f, formatted == 'recurse' || formatted == 'both', false) return { title, details, media: f } }) // sort files by leading nuber (if exists) files.sort((a, b) => { const valA = a.title.match(/\d+/) ? a.title.match(/\d+/)[0] : 0 const valB = b.title.match(/\d+/) ? b.title.match(/\d+/)[0] : 0 return parseInt(valA) - parseInt(valB) }) // remove leading number from tile & // sanitize title for html unicode characters files.forEach(f => { f.title = f.title.replace(/^\d+(\s+|_)/, '') f.title = f.title.replaceAll('/', '"') }) // if images only if (imagesOnly) { // setup object per image in array _r.images = files.filter(f => !f.media.match(/\.thumb\.[^/.]{1,4}$/i)) } else { _r.album = true _r.tracks = files } } if (imagesOnly) { _r.images = await Promise.all(_r.images.map(async i => { return { ...i, thumb: await generateThumb(i.media, path.join(mediaDir, _r.media)) } })) } // return the result to the 'obj' variable return _r })) if (order) { obj.sort((a, b) => { return a.order - b.order }) } // turn the obj variable into JSON const json = JSON.stringify(obj, null, 2) // set the output filename let outputFile = `${parent_dir}.json` // check if file already exists at location // const dataFile = await fs.readFile(path.join(outputDir, outputFile), 'utf8') // console.log(JSON.parse(dataFile) || 'no data file exists') // update only new fields // if dryrun if (dryRun) { // just console it out console.log(json) } else { // write it to a file fs.writeFile(path.join(outputDir, outputFile), json) } } function genMetadata(path, formatted) { let title, details const cleanPath = path.replace(/^\d+(_|.)/, '') // if formatted flag set if (formatted) { // split the incoming name from the map on the '_' // and set the first element to title, and the second to details let [ t, _, d ] = cleanPath.split(/(,|_)(.+)/s) // assign to return object title = t details = d ? d.replace(/\.+(mp\d|m\dv)$/i, '').trim() : '' } else { // else just set title to filename without extensions const name = cleanPath.replace(/\.\S{3,4}$/, '') title = name.replace(/_/g, ' ') } title = title.replace(/\.\w+$/, '') details = details ? details.replace(/\.\w+$/, '') : undefined return { title, details } } function linkImage(name, images) { return images.find(i => { return i.replace(/\.[^/.]+$/, '') == name }) } main()