diff --git a/public/index.js b/public/index.js index 3b69391..4658881 100644 --- a/public/index.js +++ b/public/index.js @@ -170,11 +170,11 @@ return false } - function getPosition(e) { + function getPosition (e) { var posx = 0 var posy = 0 - if (!e) var e = window.event + if (!e) e = window.event if (e.pageX || e.pageY) { posx = e.pageX @@ -192,25 +192,25 @@ } } - function positionMenu(e) { - clickCoords = getPosition(e) - clickCoordsX = clickCoords.x - clickCoordsY = clickCoords.y + function positionMenu (e) { + const clickCoords = getPosition(e) + const clickCoordsX = clickCoords.x + const clickCoordsY = clickCoords.y - menuWidth = menu.offsetWidth + 4 - menuHeight = menu.offsetHeight + 4 + const menuWidth = menu.offsetWidth + 4 + const menuHeight = menu.offsetHeight + 4 - windowWidth = window.innerWidth - windowHeight = window.innerHeight + const windowWidth = window.innerWidth + const windowHeight = window.innerHeight - 80 - if ( (windowWidth - clickCoordsX) < menuWidth ) { - menu.style.left = windowWidth - menuWidth + 'px' + if ((windowWidth - clickCoordsX) < menuWidth) { + menu.style.left = (windowWidth - menuWidth) + 'px' } else { menu.style.left = clickCoordsX + 'px' } - if ( (windowHeight - clickCoordsY) < menuHeight ) { - menu.style.top = windowHeight - menuHeight + 'px' + if ((windowHeight - clickCoordsY) < menuHeight) { + menu.style.top = (windowHeight - menuHeight) + 'px' } else { menu.style.top = clickCoordsY + 'px' } @@ -330,7 +330,6 @@ } function ctxTrack (xid, ev, qe) { - positionMenu(ev) menu.style.display = 'block' let qbtn = menu.querySelector('.ctx-item[data-action="queue"]') @@ -345,6 +344,8 @@ let plDel = menu.querySelector('.ctx-item[data-action="playlist-remove"]') plDel.style.display = (playlist != null && playlist >= 0) ? 'block' : 'none' + positionMenu(ev) + ctxState = xid } @@ -825,7 +826,7 @@ ctxHide() }, false) - document.addEventListener('click', function(e) { + document.addEventListener('click', function (e) { var clickeElIsLink = clickInsideElement(e, 'ctx-item') if (clickeElIsLink) { @@ -833,7 +834,7 @@ ctxHandle(clickeElIsLink) } else { var button = e.which || e.button - if ( button === 1 ) { + if (button === 1) { ctxHide() } } diff --git a/src/common/download.js b/src/common/download.js index 74b7a4f..583c452 100644 --- a/src/common/download.js +++ b/src/common/download.js @@ -1,8 +1,7 @@ -import {spawn} from 'child_process' +import { spawn } from 'child_process' import ffmpeg from 'fluent-ffmpeg' import path from 'path' -import asn from './async' function parseTitle (data) { let tt = data.title @@ -11,16 +10,16 @@ function parseTitle (data) { tt = tt.replace(/^\[\w+\]\s?/i, '') // Remove "Official Video/Audio" tag - tt = tt.replace(/\s?(?:[\(\)\[\]])?Official\s?[\w]+(?:[\(\)\[\]])?/i, '') + tt = tt.replace(/\s?(?:[()[]])?Official\s?[\w]+(?:[()[]])?/i, '') // Remove "Audio" tag - tt = tt.replace(/\s?(?:[\(\)\[\]])Audio?(?:[\(\)\[\]])/i, '') + tt = tt.replace(/\s?(?:[()[]])Audio?(?:[()[]])/i, '') // Remove "lyrics" tag - tt = tt.replace(/\s?(?:[\(\)\[\]])?lyrics?\s?(?:[\w]+)?(?:[\(\)\[\]])?\s?/i, '') + tt = tt.replace(/\s?(?:[()[]])?lyrics?\s?(?:[\w]+)?(?:[()[]])?\s?/i, '') // Artist / Title split - let at = tt.split(' - ', 2) + const at = tt.split(' - ', 2) let artist let title @@ -39,7 +38,7 @@ function parseTitle (data) { } function getVideoInfo (arg) { - let yt = spawn('youtube-dl', ['--no-playlist', '-j', '-f', 'bestaudio/best', arg]) + const yt = spawn('youtube-dl', ['--no-playlist', '-j', '-f', 'bestaudio/best', arg]) let output = '' yt.stdout.on('data', function (chunk) { @@ -48,10 +47,10 @@ function getVideoInfo (arg) { return new Promise((resolve, reject) => { yt.on('close', function () { - let ftdata = output.trim().split('\n') + const ftdata = output.trim().split('\n') if (ftdata.length > 1) { - let composite = [] - for (let i in ftdata) { + const composite = [] + for (const i in ftdata) { let dat try { dat = JSON.parse(ftdata[i]) @@ -65,7 +64,7 @@ function getVideoInfo (arg) { return resolve(composite) } - let data = JSON.parse(output) + const data = JSON.parse(output) delete data.formats resolve(data) }) @@ -74,7 +73,7 @@ function getVideoInfo (arg) { function fetchVideo (data) { return new Promise((resolve, reject) => { - let tempName = path.join(process.cwd(), `/tmp.yt.${data.id}.mp3`) + const tempName = path.join(process.cwd(), `/tmp.yt.${data.id}.mp3`) ffmpeg(data.url || data.file) .audioCodec('libmp3lame') .format('mp3') diff --git a/src/dbpopulate.js b/src/dbpopulate.js index b29f548..7412d9e 100644 --- a/src/dbpopulate.js +++ b/src/dbpopulate.js @@ -1,4 +1,3 @@ -import fs from 'fs-extra' import path from 'path' import readline from 'readline' @@ -12,33 +11,35 @@ const musicdir = path.resolve(values.directory) // ffprobe -i -show_entries format=duration -v quiet -of csv="p=0" async function interactive (fpath) { - let rl = readline.createInterface({ + const 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 = { + const pt = path.parse(fpath) + const track = { file: fpath, title: pt.name } - let clean = dl.parseTitle(track) + const clean = dl.parseTitle(track) console.log('== Determined Title: ' + clean.title) console.log('== Determined Artist: ' + clean.artist) - let newTitle = await asn.askAsync(rl, `Title [${clean.title}] ? `) - let newArtist = await asn.askAsync(rl, `Artist [${clean.artist}] ? `) + const newTitle = await asn.askAsync(rl, `Title [${clean.title}] ? `) + const newArtist = await asn.askAsync(rl, `Artist [${clean.artist}] ? `) - if (newTitle.trim() !== '') + if (newTitle.trim() !== '') { track.title = newTitle + } - if (newArtist.trim() !== '') + if (newArtist.trim() !== '') { track.artist = newArtist + } - let len = await asn.promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`) + const len = await asn.promiseExec(`ffprobe -i "${track.file}" -show_entries format=duration -v quiet -of csv="p=0"`) track.duration = parseFloat(len) rl.close() @@ -47,7 +48,7 @@ async function interactive (fpath) { } async function handlePassed (db, fpath) { - let filePath = path.resolve(fpath) + const filePath = path.resolve(fpath) let trackinf try { @@ -60,18 +61,18 @@ async function handlePassed (db, fpath) { throw new Error('Nothing to do.') } - let ins = await asn.insertDB(db, trackinf) + const ins = await asn.insertDB(db, trackinf) if (!ins) { throw new Error('A track of this description already exists in the database.') } } async function run () { - let db = await dbPromise + const db = await dbPromise if (process.argv[2] != null) { for (let i = 2; i < process.argv.length; i++) { - let f = process.argv[i] + const f = process.argv[i] console.log('=> Passing', f) try { await handlePassed(db, f) @@ -84,16 +85,16 @@ async function run () { return } - let files = await asn.getFiles(musicdir) - let cleanTrackData = [] + const files = await asn.getFiles(musicdir) + const cleanTrackData = [] let skips = 0 - for (let i in files) { - let file = files[i] + for (const i in files) { + const 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 asn.getInfos(file) + const fd = await asn.getInfos(file) cleanTrackData.push(fd) } catch (e) { skips++ @@ -103,11 +104,11 @@ async function run () { 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] + for (const i in cleanTrackData) { + const track = cleanTrackData[i] process.stdout.write(`\rPopulating database.. (Track ${parseInt(i) + 1} of ${cleanTrackData.length})`) try { - let ins = await asn.insertDB(db, track) + const ins = await asn.insertDB(db, track) if (!ins) continue entries++ } catch (e) { diff --git a/src/download.js b/src/download.js index acc8cc7..2feb0cf 100644 --- a/src/download.js +++ b/src/download.js @@ -1,6 +1,5 @@ 'use strict' -import fs from 'fs-extra' import readline from 'readline' import path from 'path' @@ -12,19 +11,19 @@ const values = require(path.join(process.cwd(), 'values.json')) const musicdir = path.resolve(values.directory) const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout + input: process.stdin, + output: process.stdout }) async function download (furl) { console.log('=> Getting information..') - let data = await dl.getVideoInfo(furl) + const data = await dl.getVideoInfo(furl) console.log('=> Downloading file..') - let file = await dl.fetchVideo(data) + const file = await dl.fetchVideo(data) console.log('=> Cleaning up..') - let clean = dl.parseTitle(file) + const clean = dl.parseTitle(file) let filename = clean.artist + ' - ' + clean.title + '.mp3' console.log('=> Original Title: ' + file.title + '\n') @@ -32,9 +31,9 @@ async function download (furl) { console.log('== Determined Artist: ' + clean.artist) console.log('== Determined File Name: ' + filename) - 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}] ? `) + const titleAnsw = await asn.askAsync(rl, `Title [${clean.title}] ? `) + const artistAnsw = await asn.askAsync(rl, `Artist [${clean.artist}] ? `) + const fileAnsw = await asn.askAsync(rl, `File [${filename}] ? `) if (titleAnsw && titleAnsw.trim() !== '') { clean.title = titleAnsw @@ -48,14 +47,14 @@ async function download (furl) { filename = fileAnsw } - let fn = path.join(musicdir, filename) + const fn = path.join(musicdir, filename) await asn.setMetadata(fn, clean, file.source, true) - let addAnsw = await asn.askAsync(rl, `Would you like to add it to the database now? [N/y] ? `) + const addAnsw = await asn.askAsync(rl, 'Would you like to add it to the database now? [N/y] ? ') if (addAnsw && addAnsw.trim().toLowerCase().indexOf('y') === 0) { // Add to database try { - let verify = await asn.getInfos(fn) + const verify = await asn.getInfos(fn) await asn.insertDB(await dbPromise, verify) } catch (e) { console.warn('=!= Add to database failed!') diff --git a/src/external.js b/src/external.js new file mode 100644 index 0000000..6a545fa --- /dev/null +++ b/src/external.js @@ -0,0 +1,141 @@ +import path from 'path' +import asn from './common/async' +import dl from './common/download' +import { dbPromise } from './database' +import { URL } from 'url' + +const fs = require('fs').promises +const values = require(path.join(process.cwd(), 'values.json')) +const memexpire = 1800 + +const externalTracks = {} +const downloadQueue = [] +let downloading = false + +async function downloadLocally (id) { + const info = await getTrackMetaReal(id) + if (!info) throw new Error('No track with this ID in external list.') + const file = await dl.fetchVideo(info) + const filename = info.artist + ' - ' + info.title + '.mp3' + info.file = path.join(values.directory, filename) + await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${info.artist}" -metadata title="${info.title}" -codec copy "${info.file}"`) + await fs.unlink(file.source) + const db = await dbPromise + const ins = await asn.insertDB(db, info) + if (!ins) { + throw new Error('A track of this description already exists in the database.') + } +} + +async function getTrackMetaReal (id) { + if (!id || !externalTracks[id]) return null + const trdata = externalTracks[id] + + // Check for expiry + if (trdata.file && trdata.expires > Date.now()) { + return Object.assign({}, trdata) + } + + const trsrch = 'ytsearch3:' + trdata.artist + ' - ' + trdata.title + const dldata = await dl.getVideoInfo(trsrch) + + let bestMatch + if (dldata.length === 1) bestMatch = dldata[0] + + let candidates = [] + for (const i in dldata) { + const obj = dldata[i] + const title = obj.title.toLowerCase() + + // Skip any video with 'video' in it, but keep lyric videos + if (title.indexOf('video') !== -1 && title.indexOf('lyric') === -1) continue + + // If the title has 'audio' in it, it might be the best match + if (title.indexOf('audio') !== -1) { + bestMatch = obj + break + } + + candidates.push(obj) + } + + if (candidates.length && !bestMatch) { + // Sort candidates by view count + candidates = candidates.sort(function (a, b) { + return b.view_count - a.view_count + }) + + // Select the one with the most views + bestMatch = candidates[0] + } + + // If there were no suitable candidates, just take the first response + if (!candidates.length && !bestMatch) bestMatch = dldata[0] + + externalTracks[id] = { + id: trdata.id, + title: trdata.title, + artist: trdata.artist, + file: bestMatch.url, + duration: bestMatch.duration, + expires: Date.now() + memexpire * 1000, + external: true + } + + return Object.assign({}, externalTracks[id]) +} + +// Download thread +let dltd = null +function invokeDownload (add) { + if (add) downloadQueue.push(add) + if (dltd) return + dltd = setTimeout(function (argument) { + dltd = null + if (downloading) return invokeDownload() + if (!downloadQueue.length) return + downloading = true + downloadLocally(downloadQueue.shift()).then(() => { + downloading = false + if (downloadQueue.length) invokeDownload() + }).catch((e) => { + console.error(e) + downloading = false + if (downloadQueue.length) invokeDownload() + }) + }, 2 * 1000) +} + +function addListExternal (id, obj) { + if (externalTracks[id]) return Object.assign({}, externalTracks[id]) + externalTracks[id] = obj + return obj +} + +async function searchURL (url) { + const urlp = new URL(url) + if (urlp.hostname.indexOf('youtube.com') !== -1 || + urlp.hostname.indexOf('youtu.be') !== -1) { + const id = urlp.searchParams.get('v') || urlp.pathname.substr(1) + const ytb = await dl.getVideoInfo(id) + externalTracks[id] = { + id: id, + title: ytb.title, + artist: ytb.artist || ytb.uploader, + file: ytb.url, + duration: ytb.duration, + expires: Date.now() + memexpire * 1000, + external: true + } + return [externalTracks[id]] + } + return [] +} + +export default { + downloadLocally, + invokeDownload, + addListExternal, + getTrackMetaReal, + searchURL +} diff --git a/src/lastfm.js b/src/lastfm.js index a2a54ee..6ad7201 100644 --- a/src/lastfm.js +++ b/src/lastfm.js @@ -1,18 +1,11 @@ import path from 'path' import rp from 'request-promise-native' -import asn from './common/async' -import dl from './common/download' import crypto from 'crypto' import { dbPromise } from './database' +import { addListExternal } from './external' import { parseStringPromise } from 'xml2js' -const fs = require('fs').promises const values = require(path.join(process.cwd(), 'values.json')) -const memexpire = 1800 - -let externalTracks = {} -let downloadQueue = [] -let downloading = false function createHash (data) { return crypto @@ -22,79 +15,6 @@ function createHash (data) { .substr(0, 8) } -async function downloadLocally (id) { - let info = await getTrackMetaReal(id) - if (!info) throw new Error('No track with this ID in external list.') - let file = await dl.fetchVideo(info) - let filename = info.artist + ' - ' + info.title + '.mp3' - info.file = path.join(values.directory, filename) - await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${info.artist}" -metadata title="${info.title}" -codec copy "${info.file}"`) - await fs.unlink(file.source) - let db = await dbPromise - let ins = await asn.insertDB(db, info) - if (!ins) { - throw new Error('A track of this description already exists in the database.') - } -} - -async function getTrackMetaReal (id) { - if (!id || !externalTracks[id]) return null - let trdata = externalTracks[id] - - // Check for expiry - if (trdata.file && trdata.expires > Date.now()) { - return Object.assign({}, trdata) - } - - let trsrch = 'ytsearch3:' + trdata.artist + ' - ' + trdata.title - let dldata = await dl.getVideoInfo(trsrch) - - let bestMatch - if (dldata.length === 1) bestMatch = dldata[0] - - let candidates = [] - for (let i in dldata) { - let obj = dldata[i] - let title = obj.title.toLowerCase() - - // Skip any video with 'video' in it, but keep lyric videos - if (title.indexOf('video') !== -1 && title.indexOf('lyric') === -1) continue - - // If the title has 'audio' in it, it might be the best match - if (title.indexOf('audio') !== -1) { - bestMatch = obj - break - } - - candidates.push(obj) - } - - if (candidates.length && !bestMatch) { - // Sort candidates by view count - candidates = candidates.sort(function (a, b) { - return b.view_count - a.view_count - }) - - // Select the one with the most views - bestMatch = candidates[0] - } - - // If there were no suitable candidates, just take the first response - if (!candidates.length && !bestMatch) bestMatch = dldata[0] - - externalTracks[id] = { - id: trdata.id, - title: trdata.title, - artist: trdata.artist, - file: bestMatch.url, - duration: bestMatch.duration, - expires: Date.now() + memexpire * 1000, - external: true - } - - return Object.assign({}, externalTracks[id]) -} - async function search (track, limit = 30) { if (!values.lastfm) return [] @@ -110,9 +30,9 @@ async function search (track, limit = 30) { return [] } - let final = [] - for (let i in data.results.trackmatches.track) { - let res = data.results.trackmatches.track[i] + const final = [] + for (const i in data.results.trackmatches.track) { + const res = data.results.trackmatches.track[i] let clean = { id: createHash(res), artist: res.artist, @@ -121,13 +41,7 @@ async function search (track, limit = 30) { mbid: res.mbid } - if (externalTracks[clean.id]) { - // Copy object - clean = Object.assign({}, externalTracks[clean.id]) - } else { - // Save in cache - externalTracks[clean.id] = clean - } + clean = addListExternal(clean.id, clean) final.push(clean) } @@ -135,42 +49,21 @@ async function search (track, limit = 30) { return final } -// Download thread -let dltd = null -function invokeDownload (add) { - if (add) downloadQueue.push(add) - if (dltd) return - dltd = setTimeout(function (argument) { - dltd = null - if (downloading) return invokeDownload() - if (!downloadQueue.length) return - downloading = true - downloadLocally(downloadQueue.shift()).then(() => { - downloading = false - if (downloadQueue.length) invokeDownload() - }).catch((e) => { - console.error(e) - downloading = false - if (downloadQueue.length) invokeDownload() - }) - }, 2 * 1000) -} - // Authentication function getAPISig (params) { let allStrings = [] - let qs = {} - params['api_key'] = values.lastfm.key - for (let key in params) { - let val = params[key] + const qs = {} + params.api_key = values.lastfm.key + for (const key in params) { + const val = params[key] if (val == null || val === '') continue allStrings.push(key + val) qs[key] = val } allStrings = allStrings.sort() allStrings.push(values.lastfm.secret) - qs['api_sig'] = crypto.createHash('md5').update(allStrings.join('')).digest('hex') + qs.api_sig = crypto.createHash('md5').update(allStrings.join('')).digest('hex') return qs } @@ -179,18 +72,18 @@ function getAuthURL () { } async function getSession (token) { - let sessSig = getAPISig({ token, method: 'auth.getSession' }) - let res = await rp('http://ws.audioscrobbler.com/2.0/', { qs: sessSig }) - let rep = await parseStringPromise(res) - let name = rep.lfm.session[0].name[0] - let key = rep.lfm.session[0].key[0] + const sessSig = getAPISig({ token, method: 'auth.getSession' }) + const res = await rp('http://ws.audioscrobbler.com/2.0/', { qs: sessSig }) + const rep = await parseStringPromise(res) + const name = rep.lfm.session[0].name[0] + const key = rep.lfm.session[0].key[0] return { name, key } } async function storeSession (userId, session) { if (!session.name || !session.key) throw new Error('Invalid session parameter.') - let db = await dbPromise - let existing = await db.get('SELECT * FROM LastFM WHERE userId = ?', userId) + const db = await dbPromise + const existing = await db.get('SELECT * FROM LastFM WHERE userId = ?', userId) if (existing) { await db.run('UPDATE LastFM SET name = ?, key = ? WHERE userId = ?', session.name, session.key, userId) } else { @@ -200,19 +93,19 @@ async function storeSession (userId, session) { } async function disregardSession (userId) { - let db = await dbPromise + const db = await dbPromise return db.run('DELETE FROM LastFM WHERE userId = ?', userId) } async function getSessionForUser (userId) { - let db = await dbPromise + const db = await dbPromise return db.get('SELECT * FROM LastFM WHERE userId = ?', userId) } async function scrobbleTrack (userId, trackData) { - let sess = await getSessionForUser(userId) + const sess = await getSessionForUser(userId) if (!sess) throw new Error('User does not have a LastFM session.') - let scrobbleSig = getAPISig({ + const scrobbleSig = getAPISig({ sk: sess.key, method: 'track.scrobble', artist: trackData.artist || 'Unknown', @@ -228,5 +121,13 @@ async function scrobbleTrack (userId, trackData) { return true } -export default { search, getTrackMetaReal, invokeDownload, getAPISig, getAuthURL, getSession, - getSessionForUser, storeSession, disregardSession, scrobbleTrack } +export default { + search, + getAPISig, + getAuthURL, + getSession, + getSessionForUser, + storeSession, + disregardSession, + scrobbleTrack +} diff --git a/src/playlist.js b/src/playlist.js index e1e4080..f64e450 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -1,34 +1,33 @@ -import path from 'path' import { dbPromise } from './database' async function getPlaylist (id, order = 'ntry.indx', direction = 'ASC') { - let db = await dbPromise - let p = await db.get('SELECT * FROM Playlist WHERE id = ?', id) + const db = await dbPromise + const p = await db.get('SELECT * FROM Playlist WHERE id = ?', id) if (!p) throw new Error('No such playlist!') - let q = await db.all('SELECT ntry.id,ntry.trackId,ntry.playlistId,ntry.indx,ntry.userId, \ - trck.title,trck.artist,trck.genre,trck.year,trck.duration,trck.track,trck.album \ - FROM PlaylistEntry ntry LEFT JOIN Track \ - trck ON ntry.trackId = trck.id WHERE ntry.playlistId = ? ORDER BY ' + order + ' ' + direction.toUpperCase(), id) + const q = await db.all(`SELECT ntry.id,ntry.trackId,ntry.playlistId,ntry.indx,ntry.userId, + trck.title,trck.artist,trck.genre,trck.year,trck.duration,trck.track,trck.album + FROM PlaylistEntry ntry INNER JOIN Track + trck ON ntry.trackId = trck.id WHERE ntry.playlistId = ? ORDER BY ${order} ${direction}`, id) p.tracks = q || [] return p } async function getPlaylists (userId) { - let db = await dbPromise + const db = await dbPromise return db.all('SELECT * FROM Playlist WHERE userId = ? OR userId = NULL', userId) } async function createPlaylist (userId, title) { - let db = await dbPromise - let alreadyExists = await db.get('SELECT * FROM Playlist WHERE userId = ? AND title = ?', userId, title) + const db = await dbPromise + const alreadyExists = await db.get('SELECT * FROM Playlist WHERE userId = ? AND title = ?', userId, title) if (alreadyExists) throw new Error('Playlist with the same name already exists!') await db.run('INSERT INTO Playlist (title,userId) VALUES (?,?)', title, userId) return db.get('SELECT id FROM Playlist WHERE title = ? AND userId = ?', title, userId) } async function deletePlaylist (userId, playlistId) { - let db = await dbPromise - let ply = await db.get('SELECT * FROM Playlist WHERE id = ?', playlistId) + const db = await dbPromise + const ply = await db.get('SELECT * FROM Playlist WHERE id = ?', playlistId) if (!ply) throw new Error('Could not find playlist specified.') if (ply.userId !== userId) throw new Error(ply.userId == null ? 'This public playlist cannot be deleted through the interface.' : 'Permission denied.') db.run('DELETE Playlist WHERE id = ?', playlistId) @@ -37,14 +36,14 @@ async function deletePlaylist (userId, playlistId) { } async function addTrack (userId, playlistId, trackId) { - let db = await dbPromise - let p = await getPlaylist(playlistId) + const db = await dbPromise + const p = await getPlaylist(playlistId) if (!p) throw new Error('Invalid playlist.') if (p.userId != null && p.userId !== userId) throw new Error('Permission denied.') let alreadyExists = false - for (let i in p.tracks) { - let tr = p.tracks[i] + for (const i in p.tracks) { + const tr = p.tracks[i] if (tr.trackId === parseInt(trackId)) { alreadyExists = true break @@ -57,29 +56,29 @@ async function addTrack (userId, playlistId, trackId) { } async function removeTrack (userId, playlistId, trackId) { - let db = await dbPromise - let p = await getPlaylist(playlistId) + const db = await dbPromise + const p = await getPlaylist(playlistId) if (!p) throw new Error('Invalid playlist.') if (p.userId != null && p.userId !== userId) throw new Error('Permission denied.') - let trackEntry = await db.get('SELECT * FROM PlaylistEntry WHERE playlistId = ? AND trackId = ?', playlistId, trackId) + const trackEntry = await db.get('SELECT * FROM PlaylistEntry WHERE playlistId = ? AND trackId = ?', playlistId, trackId) if (!trackEntry) throw new Error('This track is not in the playlist.') return db.run('DELETE FROM PlaylistEntry WHERE id = ?', trackEntry.id) } async function moveTrack (userId, playlistId, trackId, position) { - let db = await dbPromise - let p = await getPlaylist(playlistId) + const db = await dbPromise + const p = await getPlaylist(playlistId) if (!p) throw new Error('Invalid playlist.') if (p.userId != null && p.userId !== userId) throw new Error('Permission denied.') - let trackEntry = await db.get('SELECT * FROM PlaylistEntry WHERE playlistId = ? AND trackId = ?', playlistId, trackId) + const trackEntry = await db.get('SELECT * FROM PlaylistEntry WHERE playlistId = ? AND trackId = ?', playlistId, trackId) if (!trackEntry) throw new Error('This track is not in the playlist.') - let trcksNew = [] - for (let i in p.tracks) { - let trck = p.tracks[i] + const trcksNew = [] + for (const i in p.tracks) { + const trck = p.tracks[i] if (trck.trackId === trackId) { trck.indx = position continue @@ -91,8 +90,8 @@ async function moveTrack (userId, playlistId, trackId, position) { } // Update indexes - for (let i in trcksNew) { - let trck = trcksNew[i] + for (const i in trcksNew) { + const trck = trcksNew[i] await db.run('UPDATE PlaylistEntry SET indx = ? WHERE trackId = ? AND playlistId = ?', trck.indx, trck.trackId, playlistId) } diff --git a/src/server.js b/src/server.js index 6096c7d..b594d2f 100644 --- a/src/server.js +++ b/src/server.js @@ -5,7 +5,6 @@ import bodyParser from 'body-parser' import connectSession from 'connect-redis' import redis from 'redis' import http from 'http' -import https from 'https' import ffmpeg from 'fluent-ffmpeg' import { user, userMiddleware } from './user' import { dbPromise } from './database' @@ -14,6 +13,7 @@ import asn from './common/async' import playlist from './playlist' import lastfm from './lastfm' +import external from './external.js' require('express-async-errors') @@ -40,7 +40,7 @@ const router = express.Router() const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year'] const srchcategories = ['title', 'artist', 'album'] -let SessionStore = connectSession(session) +const SessionStore = connectSession(session) app.use(session({ key: values.session_key || 'Session', secret: values.session_secret || 'ch4ng3 m3!', @@ -73,17 +73,17 @@ router.get('/tracks', userMiddleware, async (req, res) => { sortdir = 'asc' } - let db = await dbPromise - let count = (await db.get('SELECT COUNT(*) FROM Track'))['COUNT(*)'] + const db = await dbPromise + const count = (await db.get('SELECT COUNT(*) as \'count\' FROM Track')).count - let pageCount = Math.ceil(count / tracksPerPage) + const pageCount = Math.ceil(count / tracksPerPage) if (page > pageCount) page = pageCount - let offset = (page - 1) * tracksPerPage - let tracks = await db.all(`SELECT * FROM Track ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`, tracksPerPage, offset) + const offset = (page - 1) * tracksPerPage + const tracks = await db.all(`SELECT * FROM Track ORDER BY ${sort} ${sortdir} LIMIT ? OFFSET ?`, tracksPerPage, offset) - for (let i in tracks) { + for (const i in tracks) { delete tracks[i].file } @@ -93,13 +93,21 @@ router.get('/tracks', userMiddleware, async (req, res) => { }) router.get('/tracks/search', userMiddleware, async (req, res) => { + const streamable = (req.query.streamable === '1') let query = req.query.q - let streamable = (req.query.streamable === '1') let qr = '' let exact = false + if (query.indexOf('http') !== -1) { + const result = await external.searchURL(query) + res.jsonp({ + page: 1, count: 1, pageCount: 1, tracks: result + }) + return + } + if (query.indexOf(':') !== -1) { - let ctr = query.split(':') + const ctr = query.split(':') if (srchcategories.indexOf(ctr[0]) !== -1) { qr = `ifnull(${ctr[0]}, '')` @@ -108,8 +116,8 @@ router.get('/tracks/search', userMiddleware, async (req, res) => { } if (qr === '') { - for (let c in srchcategories) { - let cat = srchcategories[c] + for (const c in srchcategories) { + const cat = srchcategories[c] if (parseInt(c) !== 0) qr += ' || ' qr += `ifnull(${cat}, '')` } @@ -120,7 +128,7 @@ router.get('/tracks/search', userMiddleware, async (req, res) => { exact = true } - let original = String(query) + const original = String(query) if (!exact) query = `%${query}%` let sort = req.query.sort @@ -139,31 +147,31 @@ router.get('/tracks/search', userMiddleware, async (req, res) => { page = 1 } - let db = await dbPromise - let count = (await db.get(`SELECT COUNT(*) FROM Track WHERE ${qr} LIKE ?`, query))['COUNT(*)'] + const db = await dbPromise + let count = (await db.get(`SELECT COUNT(*) as 'count' FROM Track WHERE ${qr} LIKE ?`, query)).count let pageCount = Math.ceil(count / tracksPerPage) if (page > pageCount) page = pageCount - let offset = (page - 1) * tracksPerPage - let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`, + const offset = (page - 1) * tracksPerPage + let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? ORDER BY ${sort} ${sortdir} LIMIT ? OFFSET ?`, query, tracksPerPage, offset) let llimit = tracksPerPage - count if (streamable && page === pageCount && llimit > 1) { if (llimit < 10) llimit = 10 try { - let lfm = await lastfm.search(original, llimit) + const lfm = await lastfm.search(original, llimit) if (lfm && lfm.length) { tracks = tracks.concat(lfm) count = count + lfm.length - if (page == 0) page = 1 - if (pageCount == 0) pageCount = 1 + if (page === 0) page = 1 + if (pageCount === 0) pageCount = 1 } } catch (e) {} } - for (let i in tracks) { + for (const i in tracks) { delete tracks[i].file } @@ -173,12 +181,12 @@ router.get('/tracks/search', userMiddleware, async (req, res) => { }) router.get('/track/:id', userMiddleware, async (req, res, next) => { - let id = req.params.id + const id = req.params.id - let db = await dbPromise + const db = await dbPromise let track = await db.get('SELECT * FROM Track WHERE id = ?', id) if (!track) { - track = await lastfm.getTrackMetaReal(id) + track = await external.getTrackMetaReal(id) if (!track) throw new Error('404 track not found') } @@ -188,14 +196,14 @@ router.get('/track/:id', userMiddleware, async (req, res, next) => { }) router.post('/track/:id', userMiddleware, async (req, res, next) => { - let id = req.params.id - let meta = req.body + const id = req.params.id + const meta = req.body - let db = await dbPromise - let track = await db.get('SELECT file FROM Track WHERE id = ?', id) + const db = await dbPromise + const track = await db.get('SELECT file FROM Track WHERE id = ?', id) if (!track) throw new Error('404 track not found') - let m = await asn.setMetadata(track.file, meta) + const m = await asn.setMetadata(track.file, meta) await asn.updateDB(db, id, m.dbq) res.jsonp(m) @@ -209,39 +217,39 @@ router.post('/track/:id', userMiddleware, async (req, res, next) => { router.post('/playlist/new', userMiddleware, async (req, res, next) => { if (!req.body.title) throw new Error('Title missing from body.') - let id = await playlist.createPlaylist(req.session.user, req.body.title) - res.jsonp({success: true, playlist: id.id}) + const id = await playlist.createPlaylist(req.session.user, req.body.title) + res.jsonp({ success: true, playlist: id.id }) }) router.post('/playlist/delete/:playlistId', userMiddleware, async (req, res, next) => { - let pId = req.params.playlistId + const pId = req.params.playlistId await playlist.deletePlaylist(req.session.user, pId) - res.jsonp({success: true}) + res.jsonp({ success: true }) }) // Playlist track endpoints router.post('/playlist/track/put/:playlistId/:trackId', userMiddleware, async (req, res, next) => { - let pId = req.params.playlistId - let tId = req.params.trackId + const pId = req.params.playlistId + const tId = req.params.trackId await playlist.addTrack(req.session.user, pId, tId) - res.jsonp({success: true}) + res.jsonp({ success: true }) }) router.post('/playlist/track/remove/:playlistId/:trackId', userMiddleware, async (req, res, next) => { - let pId = req.params.playlistId - let tId = req.params.trackId + const pId = req.params.playlistId + const tId = req.params.trackId await playlist.removeTrack(req.session.user, pId, tId) - res.jsonp({success: true}) + res.jsonp({ success: true }) }) router.post('/playlist/track/move/:playlistId/:trackId', userMiddleware, async (req, res, next) => { - let pId = req.params.playlistId - let tId = req.params.trackId - let pos = parseInt(req.body.position) + const pId = req.params.playlistId + const tId = req.params.trackId + const pos = parseInt(req.body.position) if (!pos || isNaN(pos)) throw new Error('Invalid position.') await playlist.moveTrack(req.session.user, pId, tId, pos) - res.jsonp({success: true}) + res.jsonp({ success: true }) }) router.get('/playlist', userMiddleware, async (req, res, next) => { @@ -284,7 +292,7 @@ async function scrobble (user, track) { } router.get('/lastfm', userMiddleware, async (req, res, next) => { - let sess = await lastfm.getSessionForUser(req.session.user) + const sess = await lastfm.getSessionForUser(req.session.user) if (sess) return res.jsonp({ connected: true, name: sess.name }) res.jsonp({ connected: false }) }) @@ -300,20 +308,20 @@ router.get('/lastfm/disconnect', userMiddleware, async (req, res, next) => { }) router.get('/lastfm/_redirect', userMiddleware, async (req, res, next) => { - let token = req.query.token + const token = req.query.token if (!token) throw new Error('Failed to get token from LastFM!') - let session = await lastfm.getSession(token) + const session = await lastfm.getSession(token) await lastfm.storeSession(req.session.user, session) res.redirect('/?success=lastfm') }) router.post('/lastfm/scrobble/:track', userMiddleware, async (req, res, next) => { - let id = req.params.track - let user = req.session.user - let db = await dbPromise + const id = req.params.track + const user = req.session.user + const db = await dbPromise let track = await db.get('SELECT title,artist,album,duration FROM Track WHERE id = ?', id) if (!track) { - track = await lastfm.getTrackMetaReal(id) + track = await external.getTrackMetaReal(id) if (!track) throw new Error('404 file not found') } await scrobble(user, track) @@ -325,26 +333,26 @@ router.post('/lastfm/scrobble/:track', userMiddleware, async (req, res, next) => // ------------ // router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => { - let id = req.params.id - let dl = (req.query.dl === '1') - let db = await dbPromise + const id = req.params.id + const dl = (req.query.dl === '1') + const db = await dbPromise let track = await db.get('SELECT file FROM Track WHERE id = ?', id) if (!track) { - track = await lastfm.getTrackMetaReal(id) + track = await external.getTrackMetaReal(id) if (!track) throw new Error('404 file not found') if (dl) { - lastfm.invokeDownload(id) + external.invokeDownload(id) return res.end('

OK

') } - dev && console.log("Remote", track.file) + dev && console.log('Remote', track.file) return ffmpeg(track.file) .audioCodec('libmp3lame') .format('mp3') .on('error', (e) => console.error(e)) - .pipe(res, {end: true}) + .pipe(res, { end: true }) } - let fpath = path.resolve(track.file) + const fpath = path.resolve(track.file) res.set('Cache-Control', 'public, max-age=31557600') if (dl) return res.download(fpath) @@ -357,7 +365,7 @@ router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => { // ---------- // router.use((err, req, res, next) => { - let msg = err.message + const msg = err.message dev && console.error(err.stack) res.status(msg.indexOf('404') !== -1 ? 404 : 400).jsonp({ error: err.message, stack: dev ? err.stack.toString() : undefined }) }) diff --git a/src/user.js b/src/user.js index 332172f..6d40add 100644 --- a/src/user.js +++ b/src/user.js @@ -7,8 +7,8 @@ import { dbPromise } from './database' const router = express.Router() async function userInfoPublic (id) { - let db = await dbPromise - let u = await db.get('SELECT id, username, image FROM User WHERE id = ?', id) + const db = await dbPromise + const u = await db.get('SELECT id, username, image FROM User WHERE id = ?', id) if (!u) return {} return { id: u.id, @@ -18,7 +18,7 @@ async function userInfoPublic (id) { } export async function userInfo (id) { - let db = await dbPromise + const db = await dbPromise return db.get('SELECT * FROM User WHERE id = ?', id) } @@ -39,11 +39,11 @@ export function user (oauth, registrations) { if (!oauth) return router - let oauth2 = new PromiseOAuth2(oauth.clientId, oauth.clientSecret, oauth.baseUrl, oauth.authorizePath, oauth.tokenPath) + const oauth2 = new PromiseOAuth2(oauth.clientId, oauth.clientSecret, oauth.baseUrl, oauth.authorizePath, oauth.tokenPath) router.get('/login/oauth/_redirect', async (req, res) => { - let code = req.query.code - let state = req.query.state + const code = req.query.code + const state = req.query.state if (!code || !state) throw new Error('Something went wrong!') if (!req.session || !req.session.oauthState || req.session.oauthState !== state) throw new Error('Possible request forgery detected! Try again.') @@ -56,7 +56,7 @@ export function user (oauth, registrations) { throw new Error('No authorization!') } - let accessToken = tokens[2].access_token + const accessToken = tokens[2].access_token // Get user information on remote let userInfo @@ -70,8 +70,8 @@ export function user (oauth, registrations) { if (!userInfo) throw new Error('Couldn\'t get user information!') // Let's see if there's a link for this user already.. - let db = await dbPromise - let userLocal = await db.get('SELECT * FROM OAuth WHERE remoteId = ?', userInfo.id) + const db = await dbPromise + const userLocal = await db.get('SELECT * FROM OAuth WHERE remoteId = ?', userInfo.id) // User and link both exist if (userLocal) { @@ -92,7 +92,7 @@ export function user (oauth, registrations) { // Create a new user and log in await db.run('INSERT INTO User (username,email,image,created) VALUES (?,?,?,?)', userInfo.username, userInfo.email, userInfo.image, new Date()) - let newU = await db.get('SELECT * FROM User WHERE username = ?', userInfo.username) + const newU = await db.get('SELECT * FROM User WHERE username = ?', userInfo.username) if (!newU) throw new Error('Something went wrong!') @@ -103,27 +103,27 @@ export function user (oauth, registrations) { }) router.get('/login/oauth', async (req, res) => { - let state = crypto.randomBytes(16).toString('hex') + const state = crypto.randomBytes(16).toString('hex') req.session.oauthState = state return res.redirect(oauth2.getAuthorizeUrl({ - 'redirect_uri': oauth.redirectUri, - 'scope': oauth.scope, - 'response_type': 'code', - 'state': state + redirect_uri: oauth.redirectUri, + scope: oauth.scope, + response_type: 'code', + state: state })) }) router.use('/login', async (req, res, next) => { if (req.session && req.session.user) return res.redirect('/') - let header = req.get('authorization') || '' - let token = header.split(/\s+/).pop() || '' - let auth = Buffer.from(token, 'base64').toString() - let parts = auth.split(/:/) - let username = parts[0] - let password = parts[1] + const header = req.get('authorization') || '' + const token = header.split(/\s+/).pop() || '' + const auth = Buffer.from(token, 'base64').toString() + const parts = auth.split(/:/) + const username = parts[0] + const password = parts[1] - let message = oauth != null ? 'Enter \'oauth\' to log in remotely.' : 'Log in' + const message = oauth != null ? 'Enter \'oauth\' to log in remotely.' : 'Log in' req.message = message if ((!username || !password) && (username !== 'oauth' && oauth)) { @@ -134,8 +134,8 @@ export function user (oauth, registrations) { return res.redirect('/user/login/oauth') } - let db = await dbPromise - let user = await db.get('SELECT * FROM User WHERE username = ?', username) + const db = await dbPromise + const user = await db.get('SELECT * FROM User WHERE username = ?', username) if (!user) return next() if (!user.password && oauth) { @@ -143,7 +143,7 @@ export function user (oauth, registrations) { } // Compare passwords - let ures = await bcrypt.compare(password, user.password) + const ures = await bcrypt.compare(password, user.password) if (!ures) return next() // Set login success