listen by youtube url, fix context menu positioning

This commit is contained in:
Evert Prants 2020-09-26 09:06:53 +03:00
parent 7a3ad95d60
commit 710f1a5e48
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
9 changed files with 353 additions and 304 deletions

View File

@ -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()
}
}

View File

@ -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')

View File

@ -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 <file> -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) {

View File

@ -1,6 +1,5 @@
'use strict'
import fs from 'fs-extra'
import readline from 'readline'
import path from 'path'
@ -18,13 +17,13 @@ const rl = readline.createInterface({
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!')

141
src/external.js Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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('<p>OK</p><script>window.close();</script>')
}
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 })
})

View File

@ -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