311 lines
8.6 KiB
JavaScript
311 lines
8.6 KiB
JavaScript
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(/(?<!\.thumb)\.[^.]+$/))
|
|
files = files.map(f => {
|
|
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()
|