import { exec } from 'child_process' import path from 'path' import fs from 'fs-extra' import os from 'os' const supportedMetadata = ['album', 'genre', 'title', 'artist', 'year', 'track'] function filewalker (dir, done) { let results = [] fs.readdir(dir, function (err, list) { if (err) return done(err) let pending = list.length if (!pending) return done(null, results) list.forEach(function (file) { file = path.resolve(dir, file) fs.stat(file, function (err, stat) { if (err) return done(err) // If directory, execute a recursive call if (stat && stat.isDirectory()) { filewalker(file, function (err, res) { if (err) return done(err) results = results.concat(res) if (!--pending) done(null, results) }) } else { results.push(file) if (!--pending) done(null, results) } }) }) }) } async function insertDB (db, track) { const ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=? AND album=?', track.title, track.artist, track.album || '') if (ensure) { return null } await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?,?)', [track.title, track.artist, track.file, track.album || '', track.genre || null, track.track || null, track.year || null, Math.floor(track.duration), null]) return track } async function updateDB (db, id, meta) { const ref = [] const vals = [] for (const key in meta) { if (supportedMetadata.indexOf(key) === -1) continue const val = meta[key] ref.push(key + ' = ?') vals.push(val) } await db.run('UPDATE Track SET ' + ref.join(',') + ' WHERE id = ?', [...vals, id]) return meta } function getFiles (dir) { return new Promise((resolve, reject) => { filewalker(dir, (err, files) => { if (err) return reject(err) resolve(files) }) }) } function promiseExec (cmd, opts) { return new Promise((resolve, reject) => { exec(cmd, opts, function (err, stdout, stderr) { if (err) return reject(err) resolve({ stdout, stderr }) }) }) } function askAsync (rl, q) { return new Promise((resolve, reject) => { rl.question(q, resolve) }) } function copyAsync (fsrc, fdst) { return new Promise((resolve, reject) => { const source = fs.createReadStream(path.resolve(fsrc)) const dest = fs.createWriteStream(path.resolve(fdst)) source.pipe(dest) source.on('end', resolve) source.on('error', reject) }) } async function getInfos (file) { const formatData = await promiseExec(`ffprobe -i "${file}" -show_entries format -v quiet -of json`) let parsed = JSON.parse(formatData.stdout) if (!parsed || !parsed.format || !parsed.format.duration) throw new Error('Failed to parse metadata!') parsed = parsed.format const data = { file, duration: parseFloat(parsed.duration) } if (Math.floor(data.duration) === 0) throw new Error('Invalid file type!') if (parsed.tags) { for (const k in parsed.tags) { let tagtype = k.toLowerCase() let value = parsed.tags[k] if (tagtype === 'date') tagtype = 'year' if (tagtype === 'track' || tagtype === 'year') { if (value.indexOf('/') !== -1) { value = value.split('/')[0] } value = parseInt(value) } data[tagtype] = value } } if (!data.title) { const parsed = path.parse(file) data.title = parsed.name } return data } async function setMetadata (file, meta, source, cleanup = false) { if (!meta || !meta.title || meta.title === '') throw new Error('Invalid metadata provided') const sanit = [] const dbq = {} for (let key in meta) { if (supportedMetadata.indexOf(key) === -1) continue const val = meta[key] if ((key === 'artist' || key === 'title') && (!val || val === '')) continue dbq[key] = val if (key === 'year') key = 'date' sanit.push('-metadata ' + key + '="' + val + '"') } if (!sanit.length) throw new Error('Invalid metadata provided') if (!source) { const p = path.parse(file) source = path.join(os.tmpdir(), '.retmp' + p.ext) cleanup = true await copyAsync(file, source) await fs.unlink(file) } await promiseExec(`ffmpeg -i "${source}" ${sanit.join(' ')} -codec copy "${file}"`) if (cleanup) { await fs.unlink(source) } return { file, dbq } } export { getFiles, promiseExec, askAsync, insertDB, updateDB, copyAsync, getInfos, setMetadata, supportedMetadata }