collect common functions

This commit is contained in:
Evert Prants 2018-10-06 15:30:02 +03:00
parent 266f8d9a3b
commit 97ab22a5f4
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
4 changed files with 253 additions and 269 deletions

63
common/async.js Normal file
View File

@ -0,0 +1,63 @@
import {exec} from 'child_process'
import fs from 'fs-extra'
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)
}
})
})
})
}
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)
})
}
let a = {getFiles, promiseExec, askAsync}
export default a

124
common/download.js Normal file
View File

@ -0,0 +1,124 @@
import {spawn} from 'child_process'
import path from 'path'
import asn from './async'
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 getVideoInfo (arg) {
let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'bestaudio/best', arg])
let output = ''
yt.stdout.on('data', function (chunk) {
output += chunk.toString('utf8')
})
return new Promise((resolve, reject) => {
yt.on('close', function () {
let data = JSON.parse(output)
delete data.formats
resolve(data)
})
})
}
function fetchVideo (data) {
return new Promise((resolve, reject) => {
if (data.acodec !== 'mp3' || data.vcodec !== 'none') {
let tempName = path.join(__dirname, `/tmp.yt.${data.id}.mp3`)
let ffmpeg = spawn('ffmpeg', ['-hide_banner', '-i', data.url, '-codec:a', 'libmp3lame', '-q:a', 2, '-joint_stereo', 1, '-y', tempName])
ffmpeg.stdout.pipe(process.stderr)
ffmpeg.stderr.pipe(process.stderr)
ffmpeg.on('error', function (e) {
reject(e)
})
ffmpeg.on('close', function () {
resolve({
title: data.title,
artist: data.uploader,
url: data.webpage_url,
art: data.thumbnail,
source: tempName
})
})
} else {
reject(new Error('Invalid format returned.'))
}
})
}
async function getInfos (file) {
let id3 = await asn.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 asn.promiseExec(`ffprobe -i "${file}" -show_entries format=duration -v quiet -of csv="p=0"`)
len = parseFloat(len.stdout)
data.duration = len
return data
}
export default {parseTitle, getVideoInfo, fetchVideo, getInfos}

View File

@ -1,9 +1,12 @@
#!/usr/bin/env babel-node #!/usr/bin/env babel-node
import fs from 'fs-extra'
import path from 'path' import path from 'path'
import sqlite from 'sqlite' import sqlite from 'sqlite'
import Promise from 'bluebird' import Promise from 'bluebird'
import fs from 'fs-extra' import readline from 'readline'
import {exec} from 'child_process'
import asn from './common/async'
import dl from './common/download'
const values = require(path.join(__dirname, 'values.json')) const values = require(path.join(__dirname, 'values.json'))
const musicdir = path.resolve(values.directory) const musicdir = path.resolve(values.directory)
@ -12,163 +15,28 @@ const dbPromise = Promise.resolve()
.then(() => sqlite.open(path.join(__dirname, values.database), { Promise, cache: true })) .then(() => sqlite.open(path.join(__dirname, values.database), { Promise, cache: true }))
.then(db => db.migrate()) .then(db => db.migrate())
const readline = require('readline')
// ffprobe -i <file> -show_entries format=duration -v quiet -of csv="p=0" // 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()) {
//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) { async function interactive (fpath, db) {
let rl = readline.createInterface({ let rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}) })
console.log('No metadata found for specified file! Interactive mode enabled.\n') console.log('=> No metadata found for specified file! Interactive mode enabled.\n')
let pt = path.parse(fpath) let pt = path.parse(fpath)
let track = { let track = {
file: fpath, file: fpath,
title: pt.name title: pt.name
} }
let clean = parseTitle(track) let clean = dl.parseTitle(track)
console.log('Determined Title: ' + clean.title) console.log('== Determined Title: ' + clean.title)
console.log('Determined Artist: ' + clean.artist) console.log('== Determined Artist: ' + clean.artist)
let newTitle = await askAsync(rl, `Title [${clean.title}] ? `) let newTitle = await asn.askAsync(rl, `Title [${clean.title}] ? `)
let newArtist = await askAsync(rl, `Artist [${clean.artist}] ? `) let newArtist = await asn.askAsync(rl, `Artist [${clean.artist}] ? `)
if (newTitle.trim() !== '') if (newTitle.trim() !== '')
track.title = newTitle track.title = newTitle
@ -176,13 +44,7 @@ async function interactive (fpath, db) {
if (newArtist.trim() !== '') if (newArtist.trim() !== '')
track.artist = newArtist track.artist = newArtist
let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', track.title, track.artist) let len = await asn.promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`)
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) track.duration = parseFloat(len)
rl.close() rl.close()
@ -198,7 +60,7 @@ async function run () {
let trackinf let trackinf
try { try {
trackinf = await getInfos(filePath) trackinf = await dl.getInfos(filePath)
} catch (e) { } catch (e) {
trackinf = await interactive(filePath, db) trackinf = await interactive(filePath, db)
} }
@ -208,11 +70,17 @@ async function run () {
return return
} }
let ensure = await db.get('SELECT * FROM Track WHERE title=? AND artist=?', trackinf.title, trackinf.artist)
if (ensure) {
console.error('A track of this description already exists in the database.')
return
}
await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)', await db.run('INSERT INTO Track VALUES (NULL,?,?,?,?,?,?,?,?)',
[trackinf.title, trackinf.artist, trackinf.file, trackinf.album || null, trackinf.genre || null, trackinf.track || null, [trackinf.title, trackinf.artist, trackinf.file, trackinf.album || null, trackinf.genre || null, trackinf.track || null,
trackinf.year || null, Math.floor(trackinf.duration)]) trackinf.year || null, Math.floor(trackinf.duration)])
console.log('Done.') console.log('=> Done.')
return return
} }
@ -225,7 +93,7 @@ async function run () {
// if (cleanTrackData.length > 10) break // debug purposes // if (cleanTrackData.length > 10) break // debug purposes
process.stdout.write(`\rProcessing file ${parseInt(i) + 1} of ${files.length}.. (${skips} skipped)`) process.stdout.write(`\rProcessing file ${parseInt(i) + 1} of ${files.length}.. (${skips} skipped)`)
try { try {
let fd = await getInfos(file) let fd = await dl.getInfos(file)
cleanTrackData.push(fd) cleanTrackData.push(fd)
} catch (e) { } catch (e) {
skips++ skips++
@ -251,7 +119,7 @@ async function run () {
} }
} }
console.log(`\n${entries} tracks were successfully added to the cache!`) console.log(`=> \n${entries} tracks were successfully added to the cache!`)
} }
run() run()

View File

@ -1,134 +1,63 @@
#!/usr/bin/env node #!/usr/bin/env babel-node
'use strict' 'use strict'
const spawn = require('child_process').spawn import fs from 'fs-extra'
const fs = require('fs') import readline from 'readline'
import path from 'path'
const readline = require('readline') import asn from './common/async'
import dl from './common/download'
const values = require(path.join(__dirname, 'values.json'))
const musicdir = path.resolve(values.directory)
const rl = readline.createInterface({ const rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout
}) })
function download (arg, handleCb) { async function download (furl) {
let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'bestaudio/best', arg]) console.log('=> Getting information..')
let data = await dl.getVideoInfo(furl)
let output = '' console.log('=> Downloading file..')
let file = await dl.fetchVideo(data)
yt.stdout.on('data', function (chunk) { console.log('=> Cleaning up..')
output += chunk.toString('utf8') let clean = dl.parseTitle(file)
})
yt.on('close', function () {
let data = JSON.parse(output)
delete data.formats
fetchVideo(data, handleCb)
})
}
function fetchVideo (data, cb) {
console.log('audio codec:', data.acodec)
if (data.acodec !== 'mp3' || data.vcodec !== 'none') {
let tempName = __dirname + '/tmp.yt.' + data.id + '.mp3'
let ffmpeg = spawn('ffmpeg', ['-hide_banner', '-i', data.url, '-codec:a', 'libmp3lame', '-q:a', 2, '-joint_stereo', 1, '-y', tempName])
ffmpeg.stdout.pipe(process.stderr)
ffmpeg.stderr.pipe(process.stderr)
data.filename = tempName
console.log('Downloading ' + data.title + '...')
ffmpeg.on('close', function () {
outputVideo(data, cb)
})
} else {
console.log('Invalid format returned.')
cb(null)
}
}
function outputVideo (video, cb) {
cb({
title: video.title,
artist: video.uploader,
url: video.webpage_url,
art: video.thumbnail,
source: video.filename
})
}
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
}
download(process.argv[2], function (s) {
let clean = parseTitle(s)
let filename = clean.artist + ' - ' + clean.title + '.mp3' let filename = clean.artist + ' - ' + clean.title + '.mp3'
console.log('Cleaning up..') console.log('=> Original Title: ' + file.title + '\n')
console.log('Original Title: ' + s.title + '\n') console.log('== Determined Title: ' + clean.title)
console.log('Determined Title: ' + clean.title) console.log('== Determined Artist: ' + clean.artist)
console.log('Determined Artist: ' + clean.artist) console.log('== Determined File Name: ' + filename)
console.log('Determined File Name: ' + filename)
rl.question(`Title [${clean.title}] ? `, (answer) => { let titleAnsw = await asn.askAsync(rl, `Title [${clean.title}] ? `)
if (answer && answer.trim() !== '') { let artistAnsw = await asn.askAsync(rl, `Artist [${clean.artist}] ? `)
clean.title = answer let fileAnsw = await asn.askAsync(rl, `File [${filename}] ? `)
}
rl.question(`Artist [${clean.artist}] ? `, (answer) => { if (titleAnsw && titleAnsw.trim() !== '') {
if (answer && answer.trim() !== '') { clean.title = titleAnsw
clean.artist = answer }
}
rl.question(`File [${filename}] ? `, (answer) => { if (artistAnsw && artistAnsw.trim() !== '') {
if (answer && answer.trim() !== '') { clean.artist = artistAnsw
filename = answer }
}
let fn = __dirname + '/' + filename if (fileAnsw && fileAnsw.trim() !== '') {
fs.rename(s.source, fn, function (err) { filename = fileAnsw
if (err) { }
console.error(err)
return rl.close()
}
let id3 = spawn('id3', ['-a', clean.artist, '-t', clean.title, fn]) let fn = path.join(musicdir, filename)
id3.on('close', () => { await fs.rename(file.source, fn)
console.log('Saved as ' + fn)
rl.close() let id3 = await asn.promiseExec(`id3 -a "${clean.artist}" -t "${clean.title}" "${fn}"`)
})
}) console.log('=> Done.')
}) rl.close()
}) }
})
download(process.argv[2]).catch((e) => {
console.error(e.message)
rl.close()
}) })