2018-10-05 10:36:57 +00:00
|
|
|
#!/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 <file> -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()) {
|
2018-10-05 13:28:44 +00:00
|
|
|
//results.push(file)
|
2018-10-05 10:36:57 +00:00
|
|
|
|
|
|
|
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()
|