mm.site/scripts/files2json.js

311 lines
8.6 KiB
JavaScript
Raw Normal View History

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
2023-11-18 12:48:00 +00:00
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.
2023-11-18 12:48:00 +00:00
--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
2023-11-18 12:48:00 +00:00
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 = {
2023-11-18 12:48:00 +00:00
...genMetadata(m, formatted == 'parent' || formatted == 'both', order),
parent_dir,
media: m,
2023-11-18 12:48:00 +00:00
image: '',
order: 0
}
if (order) {
2023-12-04 22:07:00 +00:00
const idx = m.match(/\d+/) ? m.match(/\d+/)[0] : 0
2023-11-18 12:48:00 +00:00
_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
2023-12-04 22:07:00 +00:00
files = files.filter(f => f.match(/(?<!\.thumb)\.[^.]+$/))
files = files.map(f => {
2023-12-04 22:07:00 +00:00
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('&#47', '&#34')
})
// if images only
if (imagesOnly) {
2023-12-04 22:07:00 +00:00
// 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
}))
2023-11-18 12:48:00 +00:00
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)
}
}
2023-12-04 22:07:00 +00:00
function genMetadata(path, formatted) {
let title, details
2023-12-04 22:07:00 +00:00
const cleanPath = path.replace(/^\d+(_|.)/, '')
2023-11-18 12:48:00 +00:00
// 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
2023-12-04 22:07:00 +00:00
let [ t, _, d ] = cleanPath.split(/(,|_)(.+)/s)
// assign to return object
title = t
2023-11-18 12:48:00 +00:00
details = d ? d.replace(/\.+(mp\d|m\dv)$/i, '').trim() : ''
} else {
// else just set title to filename without extensions
2023-12-04 22:07:00 +00:00
const name = cleanPath.replace(/\.\S{3,4}$/, '')
2023-11-18 12:48:00 +00:00
title = name.replace(/_/g, ' ')
}
2023-12-04 22:07:00 +00:00
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()