collect common functions
This commit is contained in:
parent
266f8d9a3b
commit
97ab22a5f4
63
common/async.js
Normal file
63
common/async.js
Normal 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
124
common/download.js
Normal 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}
|
176
dbpopulate.js
176
dbpopulate.js
@ -1,9 +1,12 @@
|
||||
#!/usr/bin/env babel-node
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import sqlite from 'sqlite'
|
||||
import Promise from 'bluebird'
|
||||
import fs from 'fs-extra'
|
||||
import {exec} from 'child_process'
|
||||
import readline from '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)
|
||||
@ -12,163 +15,28 @@ 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()) {
|
||||
//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')
|
||||
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)
|
||||
let clean = dl.parseTitle(track)
|
||||
|
||||
console.log('Determined Title: ' + clean.title)
|
||||
console.log('Determined Artist: ' + clean.artist)
|
||||
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}] ? `)
|
||||
let newTitle = await asn.askAsync(rl, `Title [${clean.title}] ? `)
|
||||
let newArtist = await asn.askAsync(rl, `Artist [${clean.artist}] ? `)
|
||||
|
||||
if (newTitle.trim() !== '')
|
||||
track.title = newTitle
|
||||
@ -176,13 +44,7 @@ async function interactive (fpath, db) {
|
||||
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"`)
|
||||
let len = await asn.promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`)
|
||||
track.duration = parseFloat(len)
|
||||
|
||||
rl.close()
|
||||
@ -198,7 +60,7 @@ async function run () {
|
||||
let trackinf
|
||||
|
||||
try {
|
||||
trackinf = await getInfos(filePath)
|
||||
trackinf = await dl.getInfos(filePath)
|
||||
} catch (e) {
|
||||
trackinf = await interactive(filePath, db)
|
||||
}
|
||||
@ -208,11 +70,17 @@ async function run () {
|
||||
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,?,?,?,?,?,?,?,?)',
|
||||
[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.')
|
||||
console.log('=> Done.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -225,7 +93,7 @@ async function run () {
|
||||
// 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)
|
||||
let fd = await dl.getInfos(file)
|
||||
cleanTrackData.push(fd)
|
||||
} catch (e) {
|
||||
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()
|
||||
|
151
download.js
151
download.js
@ -1,134 +1,63 @@
|
||||
#!/usr/bin/env node
|
||||
#!/usr/bin/env babel-node
|
||||
'use strict'
|
||||
|
||||
const spawn = require('child_process').spawn
|
||||
const fs = require('fs')
|
||||
import fs from 'fs-extra'
|
||||
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({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
})
|
||||
|
||||
function download (arg, handleCb) {
|
||||
let yt = spawn('youtube-dl', ['--no-playlist', '--playlist-end', 1, '-j', '-f', 'bestaudio/best', arg])
|
||||
async function download (furl) {
|
||||
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) {
|
||||
output += chunk.toString('utf8')
|
||||
})
|
||||
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)
|
||||
console.log('=> Cleaning up..')
|
||||
let clean = dl.parseTitle(file)
|
||||
let filename = clean.artist + ' - ' + clean.title + '.mp3'
|
||||
|
||||
console.log('Cleaning up..')
|
||||
console.log('Original Title: ' + s.title + '\n')
|
||||
console.log('Determined Title: ' + clean.title)
|
||||
console.log('Determined Artist: ' + clean.artist)
|
||||
console.log('Determined File Name: ' + filename)
|
||||
console.log('=> Original Title: ' + file.title + '\n')
|
||||
console.log('== Determined Title: ' + clean.title)
|
||||
console.log('== Determined Artist: ' + clean.artist)
|
||||
console.log('== Determined File Name: ' + filename)
|
||||
|
||||
rl.question(`Title [${clean.title}] ? `, (answer) => {
|
||||
if (answer && answer.trim() !== '') {
|
||||
clean.title = answer
|
||||
let titleAnsw = await asn.askAsync(rl, `Title [${clean.title}] ? `)
|
||||
let artistAnsw = await asn.askAsync(rl, `Artist [${clean.artist}] ? `)
|
||||
let fileAnsw = await asn.askAsync(rl, `File [${filename}] ? `)
|
||||
|
||||
if (titleAnsw && titleAnsw.trim() !== '') {
|
||||
clean.title = titleAnsw
|
||||
}
|
||||
|
||||
rl.question(`Artist [${clean.artist}] ? `, (answer) => {
|
||||
if (answer && answer.trim() !== '') {
|
||||
clean.artist = answer
|
||||
if (artistAnsw && artistAnsw.trim() !== '') {
|
||||
clean.artist = artistAnsw
|
||||
}
|
||||
|
||||
rl.question(`File [${filename}] ? `, (answer) => {
|
||||
if (answer && answer.trim() !== '') {
|
||||
filename = answer
|
||||
if (fileAnsw && fileAnsw.trim() !== '') {
|
||||
filename = fileAnsw
|
||||
}
|
||||
|
||||
let fn = __dirname + '/' + filename
|
||||
fs.rename(s.source, fn, function (err) {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
return rl.close()
|
||||
}
|
||||
let fn = path.join(musicdir, filename)
|
||||
await fs.rename(file.source, fn)
|
||||
|
||||
let id3 = spawn('id3', ['-a', clean.artist, '-t', clean.title, fn])
|
||||
id3.on('close', () => {
|
||||
console.log('Saved as ' + fn)
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user