#!/usr/bin/env babel-node import path from 'path' import sqlite from 'sqlite' import Promise from 'bluebird' import fs from 'fs-extra' import {exec} from 'child_process' const values = require(path.join(__dirname, 'values.json')) const musicdir = path.resolve(values.directory) const dbPromise = Promise.resolve() .then(() => sqlite.open(path.join(__dirname, values.database), { Promise, cache: true })) .then(db => db.migrate()) const readline = require('readline') // ffprobe -i -show_entries format=duration -v quiet -of csv="p=0" function filewalker (dir, done) { let results = [] fs.readdir(dir, function (err, list) { if (err) return done(err) var 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()) { //results.push(file) 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) } }) }) }) } 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}) }) }) } async function getInfos (file) { let id3 = await promiseExec(`id3 "${file}"`) let prds = id3.stdout.split('\n') let data = {} // Get id3 tags for (let i in prds) { let line = prds[i] let parts = line.split(': ') if (parts.length) { let tagtype = parts[0].toLowerCase() let tagdata = line.substring(parts[0].length + 2) if (tagtype === '') continue if (tagtype === 'metadata' && tagdata === 'none found') throw new Error(`No metadata for file "${file}"!`) if (tagtype === 'track' || tagtype === 'year') { if (tagdata.indexOf('/') !== -1) { tagdata = tagdata.split('/')[0] } tagdata = parseInt(tagdata) } data[tagtype] = tagdata } } if (!data.title) { let parsed = path.parse(file) data.title = parsed.name } // Get track length let len = await promiseExec(`ffprobe -i "${file}" -show_entries format=duration -v quiet -of csv="p=0"`) len = parseFloat(len.stdout) data.duration = len return data } function parseTitle(data) { let tt = data.title // Remove []'s from the beginning tt = tt.replace(/^\[\w+\]\s?/i, '') // Remove "Official Video/Audio" tag tt = tt.replace(/\s?(?:[\(\)\[\]])?Official\s?[\w]+(?:[\(\)\[\]])?/i, '') // Remove "Audio" tag tt = tt.replace(/\s?(?:[\(\)\[\]])Audio?(?:[\(\)\[\]])/i, '') // Remove "lyrics" tag tt = tt.replace(/\s?(?:[\(\)\[\]])?lyrics?\s?(?:[\w]+)?(?:[\(\)\[\]])?\s?/i, '') // Artist / Title split let at = tt.split(' - ', 2) let artist let title if (at.length > 1) { artist = at[0] title = tt.substring(artist.length + 3) } else { artist = data.artist title = tt } data.title = title data.artist = artist return data } function askAsync (rl, q) { return new Promise((resolve, reject) => { rl.question(q, resolve) }) } async function interactive (fpath, db) { let rl = readline.createInterface({ input: process.stdin, output: process.stdout }) console.log('No metadata found for specified file! Interactive mode enabled.\n') let pt = path.parse(fpath) let track = { file: fpath, title: pt.name } let clean = parseTitle(track) console.log('Determined Title: ' + clean.title) console.log('Determined Artist: ' + clean.artist) let newTitle = await askAsync(rl, `Title [${clean.title}] ? `) let newArtist = await askAsync(rl, `Artist [${clean.artist}] ? `) if (newTitle.trim() !== '') track.title = newTitle if (newArtist.trim() !== '') track.artist = newArtist let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', track.title, track.artist) if (ensure) { console.error('A track of this description already exists in the database.') return rl.close() } let len = await promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`) track.duration = parseFloat(len) rl.close() return track } async function run () { let db = await dbPromise if (process.argv[2] != null) { let filePath = path.resolve(process.argv[2]) let trackinf try { trackinf = await getInfos(filePath) } catch (e) { trackinf = await interactive(filePath, db) } if (!trackinf) { console.error('Nothing to do.') return } await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)', [trackinf.title, trackinf.artist, trackinf.file, trackinf.album || null, trackinf.genre || null, trackinf.track || null, trackinf.year || null, Math.floor(trackinf.duration)]) console.log('Done.') return } let files = await getFiles(musicdir) let cleanTrackData = [] let skips = 0 for (let i in files) { let file = files[i] // if (cleanTrackData.length > 10) break // debug purposes process.stdout.write(`\rProcessing file ${parseInt(i) + 1} of ${files.length}.. (${skips} skipped)`) try { let fd = await getInfos(file) cleanTrackData.push(fd) } catch (e) { skips++ continue } } process.stdout.write(`\r${cleanTrackData.length} files indexed, ${skips} files were skipped. \n`) let entries = 0 for (let i in cleanTrackData) { let track = cleanTrackData[i] process.stdout.write(`\rPopulating database.. (Track ${parseInt(i) + 1} of ${cleanTrackData.length})`) try { let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', track.title, track.artist) if (ensure) continue await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)', [track.title, track.artist, track.file, track.album || null, track.genre || null, track.track || null, track.year || null, Math.floor(track.duration)]) entries++ } catch (e) { console.warn(e.message) continue } } console.log(`\n${entries} tracks were successfully added to the cache!`) } run()