Simple playlists, download automatic db insert

This commit is contained in:
Evert Prants 2019-12-17 19:52:18 +02:00
parent 5b5414a189
commit a04bd3e69b
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
12 changed files with 454 additions and 82 deletions

View File

@ -21,6 +21,7 @@ CREATE TABLE PlaylistEntry (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
playlistId INTEGER, playlistId INTEGER,
trackId INTEGER, trackId INTEGER,
indx INTEGER,
CONSTRAINT PE_fk_playlistId FOREIGN KEY (playlistId) CONSTRAINT PE_fk_playlistId FOREIGN KEY (playlistId)
REFERENCES Playlist (id) ON UPDATE CASCADE ON DELETE CASCADE, REFERENCES Playlist (id) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT PE_fk_trackId FOREIGN KEY (trackId) CONSTRAINT PE_fk_trackId FOREIGN KEY (trackId)

View File

@ -13,16 +13,18 @@
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6", "@babel/runtime": "^7.7.6",
"bcrypt": "^3.0.7", "bcrypt": "^3.0.7",
"bluebird": "^3.5.2", "bluebird": "^3.7.2",
"connect-redis": "^3.4.1", "body-parser": "^1.19.0",
"express": "^4.16.3", "connect-redis": "^3.4.2",
"express-async-errors": "^3.0.0", "express": "^4.17.1",
"express-session": "^1.16.2", "express-async-errors": "^3.1.1",
"express-session": "^1.17.0",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.0", "fs-extra": "^7.0.1",
"oauth-libre": "^0.9.17", "oauth-libre": "^0.9.17",
"socket.io": "^2.1.1", "redis": "^2.8.0",
"sqlite": "^3.0.2", "socket.io": "^2.3.0",
"sqlite": "^3.0.3",
"sqlite3": "^4.1.1" "sqlite3": "^4.1.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -177,7 +177,7 @@ tr.external td {
width: 0%; width: 0%;
background-color: #00b7ff; background-color: #00b7ff;
} }
.ctx-menu { .ctx-menu, .ctx-sub-items {
position: absolute; position: absolute;
background-color: #0c2233; background-color: #0c2233;
border: 2px solid #031421; border: 2px solid #031421;
@ -200,6 +200,17 @@ tr.external td {
.ctx-item:hover, .dropdown-content div:hover { .ctx-item:hover, .dropdown-content div:hover {
background-color: #0c273c; background-color: #0c273c;
} }
.ctx-multi {
position: relative;
}
.ctx-multi:hover > .ctx-sub-items {
display: block !important;
}
.ctx-sub-items {
left: 100%;
top: 0;
display: none !important;
}
.inline-flex { .inline-flex {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -210,6 +221,9 @@ tr.external td {
flex-grow: 1; flex-grow: 1;
min-width: 0; min-width: 0;
} }
.player .inline-flex {
overflow: hidden;
}
#search-clear { #search-clear {
display: none; display: none;
} }

View File

@ -72,6 +72,10 @@
<li><a class="ctx-item" data-action="play">Play Track</a></li> <li><a class="ctx-item" data-action="play">Play Track</a></li>
<li><a class="ctx-item" data-action="queue">Queue Track</a></li> <li><a class="ctx-item" data-action="queue">Queue Track</a></li>
<li><a class="ctx-item" data-action="download">Download</a></li> <li><a class="ctx-item" data-action="download">Download</a></li>
<li class="ctx-multi"><a class="ctx-item playlist-add" style="display: none;">Add to Playlist</a>
<ul class="ctx-sub-items playlist-list" id="ctx-playlists"></ul>
</li>
<li><a class="ctx-item" data-action="playlist-remove" style="display: none;">Remove from Playlist</a></li>
</ul> </ul>
</div> </div>
<div class="sidebar background" id="options-drop"> <div class="sidebar background" id="options-drop">

View File

@ -38,7 +38,11 @@
// Playlists // Playlists
var playlist = null var playlist = null
var playlistCache = {}
var playingList = null
var pSel = document.getElementById('playlist-select') var pSel = document.getElementById('playlist-select')
var dropdownBtn = pSel.querySelector('.dropdown-button')
var ctxPlaylist = document.getElementById('ctx-playlists')
// Options // Options
var options = { var options = {
@ -108,22 +112,38 @@
return array return array
} }
function httpGet (url, callback) { function _httpRequest (method, url, data) {
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var xmlHttp = new XMLHttpRequest() var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () { xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) { if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
resolve(JSON.parse(xmlHttp.responseText)) resolve(JSON.parse(xmlHttp.responseText))
} else if (xmlHttp.readyState === 4) { } else if (xmlHttp.readyState === 4) {
reject(new Error(xmlHttp.status)) let msg = 'Server returned code ' + xmlHttp.status
try {
let ct = JSON.parse(xmlHttp.responseText)
if (ct.error) msg = ct.error
} catch (e) {}
reject(new Error(msg))
} }
} }
xmlHttp.withCredentials = true xmlHttp.withCredentials = true
xmlHttp.open('GET', url, true) xmlHttp.open(method, url, true)
xmlHttp.send(null) if (method !== 'GET' && method !== 'HEAD') {
xmlHttp.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')
}
xmlHttp.send(data ? JSON.stringify(data) : null)
}) })
} }
function httpGet (url) {
return _httpRequest('GET', url)
}
function httpPost (url, data) {
return _httpRequest('POST', url, data)
}
function shortTitle (title, artist) { function shortTitle (title, artist) {
if (!artist) return title if (!artist) return title
return artist + ' - ' + title return artist + ' - ' + title
@ -217,7 +237,7 @@
} }
function updateQT (q) { function updateQT (q) {
if (playlist === 0) showTracks(pageNum) if (playlist === -1) showTracks(pageNum)
if (q) { if (q) {
qTag.style.display = 'block' qTag.style.display = 'block'
var cnt = '+' + queue.length var cnt = '+' + queue.length
@ -228,6 +248,25 @@
qTag.style.display = 'none' qTag.style.display = 'none'
} }
function removeFromPlaylist(track) {
if (playlist == null || playlist < 0) return
httpPost('/api/playlist/track/remove/' + playlist + '/' + track).then(function () {
alert('Track removed from playlist successfully!')
showPlaylist(playlist, true)
}).catch(function (e) {
alert(e.message)
})
}
function addToPlaylist (playlistId, trackId) {
httpPost('/api/playlist/track/put/' + playlistId + '/' + trackId).then(function () {
alert('Track added to playlist successfully!')
showPlaylist(playlistId, true)
}).catch(function (e) {
alert(e.message)
})
}
function ctxHandle (el) { function ctxHandle (el) {
if (ctxState === 0) return if (ctxState === 0) return
let dt = el.getAttribute("data-action") let dt = el.getAttribute("data-action")
@ -236,7 +275,7 @@
play(ctxState) play(ctxState)
break break
case 'queue': case 'queue':
if (playlist === 0) if (playlist === -1)
removeFromQueue(ctxState) removeFromQueue(ctxState)
else else
queue.push(ctxState) queue.push(ctxState)
@ -244,6 +283,17 @@
case 'download': case 'download':
window.open('/api/serve/by-id/' + ctxState + '?dl=1', '_blank') window.open('/api/serve/by-id/' + ctxState + '?dl=1', '_blank')
break break
case 'playlist-remove':
removeFromPlaylist(ctxState)
break
case 'playlist-new':
createPlaylist(ctxState)
break
}
// Adding to playlist
if (dt && dt.indexOf('push-') === 0) {
let id = dt.substr(5)
addToPlaylist(id, ctxState)
} }
ctxHide() ctxHide()
updateQT(qTag.style.display === 'block') updateQT(qTag.style.display === 'block')
@ -256,6 +306,12 @@
let qbtn = menu.querySelector('.ctx-item[data-action="queue"]') let qbtn = menu.querySelector('.ctx-item[data-action="queue"]')
qbtn.innerHTML = qe ? 'Remove from Queue' : 'Queue Track' qbtn.innerHTML = qe ? 'Remove from Queue' : 'Queue Track'
let plAdd = menu.querySelector('.playlist-add')
plAdd.style.display = isNaN(parseInt(xid)) ? 'none' : 'block'
let plDel = menu.querySelector('.ctx-item[data-action="playlist-remove"]')
plDel.style.display = (playlist != null && playlist >= 0) ? 'block' : 'none'
ctxState = xid ctxState = xid
} }
@ -275,7 +331,7 @@
} }
function handleHeads () { function handleHeads () {
if (playlist === 0) return if (playlist === -1) return
let els = elementsToArray(document.querySelectorAll('th')) let els = elementsToArray(document.querySelectorAll('th'))
let sortarr = document.createElement('span') let sortarr = document.createElement('span')
sortarr.className = 'order' + (options.sortdir === 'desc' ? ' dn' : '') sortarr.className = 'order' + (options.sortdir === 'desc' ? ' dn' : '')
@ -291,11 +347,13 @@
if (options.sortby !== sortattr) { if (options.sortby !== sortattr) {
options.sortdir = 'asc' options.sortdir = 'asc'
options.sortby = sortattr options.sortby = sortattr
return showTracks(pageNum) if (playlist == null || playlist < 0) return showTracks(pageNum)
showPlaylist(playlist)
} }
options.sortdir = (options.sortdir === 'asc' ? 'desc' : 'asc') options.sortdir = (options.sortdir === 'asc' ? 'desc' : 'asc')
showTracks(pageNum) if (playlist == null || playlist < 0) return showTracks(pageNum)
showPlaylist(playlist)
}) })
} }
} }
@ -308,12 +366,12 @@
let tag = trackDataRow(track) let tag = trackDataRow(track)
tag.addEventListener('click', function (e) { tag.addEventListener('click', function (e) {
play(track.id) play(track.trackId || track.id)
}, false) }, false)
tag.addEventListener('contextmenu', function (e) { tag.addEventListener('contextmenu', function (e) {
e.preventDefault() e.preventDefault()
ctxTrack(track.id, e) ctxTrack(track.trackId || track.id, e)
}, false) }, false)
table.appendChild(tag) table.appendChild(tag)
@ -324,7 +382,7 @@
function recursionQueueList (index) { function recursionQueueList (index) {
let qid = queue[index] let qid = queue[index]
if (!qid || playlist !== 0) return if (!qid || playlist !== -1) return
httpGet('/api/track/' + qid).then(function (data) { httpGet('/api/track/' + qid).then(function (data) {
let tag = trackDataRow(data) let tag = trackDataRow(data)
@ -358,6 +416,76 @@
pageNumText.innerHTML = pageNum + ' / ' + pages pageNumText.innerHTML = pageNum + ' / ' + pages
} }
function populatePlaylists () {
// Remove existing playlist names from the dropdown
let elems = elementsToArray(pSel.querySelectorAll('.playlist'))
for (let i in elems) {
let element = elems[i]
element.parentNode.removeChild(element)
}
// Get user playlists and add them to the dropdown
httpGet('/api/playlist').then(function (content) {
ctxPlaylist.innerHTML = ''
for (let i in content) {
let plylit = content[i]
// Add selection
let elem = document.createElement('div')
elem.className = 'playlist'
elem.innerHTML = plylit.title
elem.setAttribute('data-value', plylit.id)
pSel.querySelector('.dropdown-content').appendChild(elem)
// Add CTX clues
let li = document.createElement('li')
let ctxItem = document.createElement('div')
ctxItem.className = 'ctx-item'
ctxItem.innerHTML = plylit.title
ctxItem.setAttribute('data-action', 'push-' + plylit.id)
li.appendChild(ctxItem)
ctxPlaylist.appendChild(li)
}
let lit = document.createElement('li')
let ctxItemGeneral = document.createElement('div')
ctxItemGeneral.className = 'ctx-item'
ctxItemGeneral.innerHTML = 'New..'
ctxItemGeneral.setAttribute('data-action', 'playlist-new')
lit.appendChild(ctxItemGeneral)
ctxPlaylist.appendChild(lit)
handleSelect()
})
}
function showPlaylist (pid, updateOnly) {
if (!(updateOnly && playlist !== pid)) {
playlist = pid
pages = 1
pageNum = 1
}
httpGet('/api/playlist/' + pid + '?page=' + pageNum + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (content) {
playlistCache[content.id] = content
if (!(updateOnly && playlist !== pid)) {
constructList(content.tracks)
updatePaging()
}
})
}
function createPlaylist (toAdd) {
let title = window.prompt('Enter name for playlist')
if (!title || title === '') return
httpPost('/api/playlist/new', { title: title }).then(function (d) {
if (toAdd == null) alert('Playlist created successfully!')
populatePlaylists()
if (toAdd != null) {
addToPlaylist(d.playlist, toAdd)
}
}).catch(function (e) {
alert(e.message)
})
}
function showTracks (page) { function showTracks (page) {
let query = input.value let query = input.value
if (searching && query.trim() === '') { if (searching && query.trim() === '') {
@ -370,10 +498,13 @@
if (page > pages) page = pages if (page > pages) page = pages
updatePaging() updatePaging()
if (playlist === 0) { if (playlist === -1) {
return constructQueue() return constructQueue()
} }
playlist = null
dropdownBtn.innerHTML = 'All Tracks'
if (query.trim() === '') { if (query.trim() === '') {
return httpGet('/api/tracks?page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) { return httpGet('/api/tracks?page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) {
pageNum = page pageNum = page
@ -399,7 +530,7 @@
}) })
} }
function play (id, q) { function play (id, q, fromNext) {
if (id < 1) return playNext() if (id < 1) return playNext()
httpGet('/api/track/' + id).then(function (data) { httpGet('/api/track/' + id).then(function (data) {
let title = shortTitle(data.title, data.artist) let title = shortTitle(data.title, data.artist)
@ -415,19 +546,57 @@
updateQT(q) updateQT(q)
nowPlaying = id nowPlaying = id
if (!fromNext) {
if (playlist != null && playlist > -1 && !q) {
playingList = playlist
} else {
playingList = null
}
}
}, function (e) { }, function (e) {
console.log(e) console.log(e)
}) })
} }
function playNext () { function playPrevious () {
if (queue.length !== 0) return play(queue.shift(), true) if (playingList) {
let pl = playlistCache[playingList]
if (pl) {
let prevTrack = null
for (let i in pl.tracks) {
let tr = pl.tracks[i]
if (tr.trackId === nowPlaying) {
prevTrack = parseInt(i) - 1
break
}
}
if (prevTrack == null || !pl.tracks[prevTrack]) return
return play(pl.tracks[prevTrack].trackId, false, true)
}
}
if (externalStream) return if (externalStream) return
play(nowPlaying + 1) play(nowPlaying - 1, false, true)
} }
function showPlaylist (pid) { function playNext () {
if (queue.length !== 0) return play(queue.shift(), true, true)
if (playingList) {
let pl = playlistCache[playingList]
if (pl) {
let nextTrack = null
for (let i in pl.tracks) {
let tr = pl.tracks[i]
if (tr.trackId === nowPlaying) {
nextTrack = parseInt(i) + 1
break
}
}
if (nextTrack == null || !pl.tracks[nextTrack]) return
return play(pl.tracks[nextTrack].trackId, false, true)
}
}
if (externalStream) return
play(nowPlaying + 1, false, true)
} }
var ignoreFirst = false var ignoreFirst = false
@ -441,33 +610,37 @@
} }
function handleSelect () { function handleSelect () {
let btn = pSel.querySelector('.dropdown-button')
let btns = elementsToArray(pSel.querySelectorAll('.dropdown-content div')) let btns = elementsToArray(pSel.querySelectorAll('.dropdown-content div'))
for (let i in btns) { for (let i in btns) {
let btni = btns[i] let btni = btns[i]
let cnt = btni.getAttribute('data-value') let cnt = btni.getAttribute('data-value')
if (btni.getAttribute('data-bound')) continue
btni.addEventListener('click', function (e) { btni.addEventListener('click', function (e) {
let last = btn.innerHTML let isPlaylist = false
let last = dropdownBtn.innerHTML
let set = btni.innerHTML let set = btni.innerHTML
switch (cnt) { switch (cnt) {
case 'all': case 'all':
playlist = null playlist = null
break break
case 'queue': case 'queue':
playlist = 0 playlist = -1
break break
case 'options': case 'options':
toggleOptions() toggleOptions()
set = last set = last
break break
default: default:
isPlaylist = true
showPlaylist(cnt) showPlaylist(cnt)
} }
btn.innerHTML = set dropdownBtn.innerHTML = set
showTracks(pageNum) if (!isPlaylist) showTracks(pageNum)
}, false) }, false)
btni.setAttribute('data-bound', true)
} }
} }
@ -532,9 +705,7 @@
document.getElementById('player-next').addEventListener('click', playNext, false) document.getElementById('player-next').addEventListener('click', playNext, false)
document.getElementById('player-prev').addEventListener('click', function (e) { document.getElementById('player-prev').addEventListener('click', playPrevious, false)
play(nowPlaying - 1)
}, false)
document.getElementById('jump-first').addEventListener('click', function (e) { document.getElementById('jump-first').addEventListener('click', function (e) {
showTracks(1) showTracks(1)
@ -600,6 +771,7 @@
loadOptions() loadOptions()
showTracks(1) showTracks(1)
handleHash(window.location.hash) handleHash(window.location.hash)
populatePlaylists()
handleSelect() handleSelect()
handleOptions() handleOptions()
})() })()

11
src/database.js Normal file
View File

@ -0,0 +1,11 @@
import path from 'path'
import sqlite from 'sqlite'
import Promise from 'bluebird'
const values = require(path.join(process.cwd(), 'values.json'))
const dbPromise = Promise.resolve()
.then(() => sqlite.open(path.join(process.cwd(), values.database), { Promise, cache: true }))
.then(db => db.migrate())
export { dbPromise }

View File

@ -1,22 +1,17 @@
import fs from 'fs-extra' import fs from 'fs-extra'
import path from 'path' import path from 'path'
import sqlite from 'sqlite'
import Promise from 'bluebird'
import readline from 'readline' import readline from 'readline'
import asn from './common/async' import asn from './common/async'
import dl from './common/download' import dl from './common/download'
import { dbPromise } from './database'
const values = require(path.join(process.cwd(), 'values.json')) const values = require(path.join(process.cwd(), 'values.json'))
const musicdir = path.resolve(values.directory) const musicdir = path.resolve(values.directory)
const dbPromise = Promise.resolve()
.then(() => sqlite.open(path.join(process.cwd(), values.database), { Promise, cache: true }))
.then(db => db.migrate())
// ffprobe -i <file> -show_entries format=duration -v quiet -of csv="p=0" // ffprobe -i <file> -show_entries format=duration -v quiet -of csv="p=0"
async function interactive (fpath, db) { async function interactive (fpath) {
let rl = readline.createInterface({ let rl = readline.createInterface({
input: process.stdin, input: process.stdin,
output: process.stdout output: process.stdout

View File

@ -6,6 +6,7 @@ import path from 'path'
import asn from './common/async' import asn from './common/async'
import dl from './common/download' import dl from './common/download'
import { dbPromise } from './database'
const values = require(path.join(process.cwd(), 'values.json')) const values = require(path.join(process.cwd(), 'values.json'))
const musicdir = path.resolve(values.directory) const musicdir = path.resolve(values.directory)
@ -52,6 +53,18 @@ async function download (furl) {
await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${clean.artist}" -metadata title="${clean.title}" -codec copy "${fn}"`) await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${clean.artist}" -metadata title="${clean.title}" -codec copy "${fn}"`)
await fs.unlink(file.source) await fs.unlink(file.source)
let 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 dl.getInfos(fn)
await asn.insertDB(await dbPromise, verify)
} catch (e) {
console.warn('=!= Add to database failed!')
console.error(e.stack)
}
}
console.log('=> Done.') console.log('=> Done.')
rl.close() rl.close()
} }

View File

@ -2,6 +2,7 @@ import path from 'path'
import asn from './common/async' import asn from './common/async'
import dl from './common/download' import dl from './common/download'
import crypto from 'crypto' import crypto from 'crypto'
import { dbPromise } from './database'
const fs = require('fs').promises const fs = require('fs').promises
const values = require(path.join(process.cwd(), 'values.json')) const values = require(path.join(process.cwd(), 'values.json'))
@ -10,7 +11,6 @@ const memexpire = 1800
let externalTracks = {} let externalTracks = {}
let downloadQueue = [] let downloadQueue = []
let downloading = false let downloading = false
let dbPromise
function createHash (data) { function createHash (data) {
return crypto return crypto
@ -154,7 +154,4 @@ function invokeDownload (add) {
}, 2 * 1000) }, 2 * 1000)
} }
export default function (db) { export default { search, getTrackMetaReal, invokeDownload }
dbPromise = db
return { search, getTrackMetaReal, invokeDownload }
}

102
src/playlist.js Normal file
View File

@ -0,0 +1,102 @@
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)
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)
p.tracks = q || []
return p
}
async function getPlaylists (userId) {
let 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)
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)
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)
db.run('DELETE PlaylistEntry WHERE playlistId = ?', playlistId)
return true
}
async function addTrack (userId, playlistId, trackId) {
let db = await dbPromise
let 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]
if (tr.trackId === parseInt(trackId)) {
alreadyExists = true
break
}
}
if (alreadyExists) throw new Error('This track is already in the playlist.')
return db.run('INSERT INTO PlaylistEntry (playlistId,trackId,userId,indx) VALUES (?,?,?,?)', playlistId, trackId, userId, p.tracks.length)
}
async function removeTrack (userId, playlistId, trackId) {
let db = await dbPromise
let 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)
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)
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)
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]
if (trck.trackId === trackId) {
trck.indx = position
continue
}
if (trck.indx >= position) {
trck.indx++
}
trcksNew.push(trck)
}
// Update indexes
for (let i in trcksNew) {
let trck = trcksNew[i]
await db.run('UPDATE PlaylistEntry SET indx = ? WHERE trackId = ? AND playlistId = ?', trck.indx, trck.trackId, playlistId)
}
return true
}
export default { getPlaylist, getPlaylists, createPlaylist, deletePlaylist, addTrack, removeTrack, moveTrack }

View File

@ -1,50 +1,48 @@
import path from 'path' import path from 'path'
import sqlite from 'sqlite'
import Promise from 'bluebird'
import express from 'express' import express from 'express'
import session from 'express-session' import session from 'express-session'
import redis from 'connect-redis' import bodyParser from 'body-parser'
import connectSession from 'connect-redis'
import redis from 'redis'
import http from 'http' import http from 'http'
import https from 'https' import https from 'https'
import ffmpeg from 'fluent-ffmpeg' import ffmpeg from 'fluent-ffmpeg'
import { user, userMiddleware } from './user' import { user, userMiddleware } from './user'
import { dbPromise } from './database'
import lfmda from './lastfm' import playlist from './playlist'
import lastfm from './lastfm'
require('express-async-errors') require('express-async-errors')
const values = require(path.join(process.cwd(), 'values.json')) const values = require(path.join(process.cwd(), 'values.json'))
const tracksPerPage = 100 const tracksPerPage = 100
const dbPromise = Promise.resolve()
.then(() => sqlite.open(path.join(process.cwd(), values.database), { Promise, cache: true }))
.then(db => db.migrate())
const app = express() const app = express()
const port = process.env.PORT || 3000 const port = process.env.PORT || 3000
const dev = process.env.NODE_ENV === 'development' const dev = process.env.NODE_ENV === 'development'
const server = http.createServer(app) const server = http.createServer(app)
const lastfm = lfmda(dbPromise)
if (dev) { if (dev) {
const morgan = require('morgan') const morgan = require('morgan')
app.use(morgan('dev')) app.use(morgan('dev'))
} }
app.set('trust proxy', 1) app.set('trust proxy', 1)
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
const router = express.Router() const router = express.Router()
const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year', 'file'] const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year']
const srchcategories = ['title', 'artist', 'album'] const srchcategories = ['title', 'artist', 'album']
let SessionStore = redis(session) let SessionStore = connectSession(session)
app.use(session({ app.use(session({
key: values.session_key || 'Session', key: values.session_key || 'Session',
secret: values.session_secret || 'ch4ng3 m3!', secret: values.session_secret || 'ch4ng3 m3!',
store: new SessionStore(values.redis || { port: 6379 }), store: new SessionStore({ client: redis.createClient(values.redis || { port: 6379 }) }),
resave: false, resave: false,
saveUninitialized: true, saveUninitialized: true,
cookie: { cookie: {
@ -53,6 +51,10 @@ app.use(session({
} }
})) }))
// ------ //
// TRACKS //
// ------ //
router.get('/tracks', userMiddleware, async (req, res) => { router.get('/tracks', userMiddleware, async (req, res) => {
let page = parseInt(req.query.page) || 1 let page = parseInt(req.query.page) || 1
if (isNaN(page)) { if (isNaN(page)) {
@ -175,7 +177,7 @@ router.get('/track/:id', userMiddleware, async (req, res, next) => {
let track = await db.get('SELECT * FROM Track WHERE id = ?', id) let track = await db.get('SELECT * FROM Track WHERE id = ?', id)
if (!track) { if (!track) {
track = await lastfm.getTrackMetaReal(id) track = await lastfm.getTrackMetaReal(id)
if (!track) return next(new Error('404 file not found')) if (!track) throw new Error('404 track not found')
} }
delete track.file delete track.file
@ -183,26 +185,77 @@ router.get('/track/:id', userMiddleware, async (req, res, next) => {
res.jsonp(track) res.jsonp(track)
}) })
router.get('/playlists', userMiddleware, async (req, res, next) => { // --------- //
let db = await dbPromise // PLAYLISTS //
let playlists = await db.all('SELECT * FROM Playlist WHERE userId = ? OR userId = NULL', req.session.user) // --------- //
res.jsonp(playlists) // General playlist endpoints
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})
})
router.post('/playlist/delete/:playlistId', userMiddleware, async (req, res, next) => {
let pId = req.params.playlistId
await playlist.deletePlaylist(req.session.user, pId)
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
await playlist.addTrack(req.session.user, pId, tId)
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
await playlist.removeTrack(req.session.user, pId, tId)
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)
if (!pos || isNaN(pos)) throw new Error('Invalid position.')
await playlist.moveTrack(req.session.user, pId, tId, pos)
res.jsonp({success: true})
})
router.get('/playlist', userMiddleware, async (req, res, next) => {
res.jsonp(await playlist.getPlaylists(req.session.user))
}) })
router.get('/playlist/:id', userMiddleware, async (req, res, next) => { router.get('/playlist/:id', userMiddleware, async (req, res, next) => {
let id = req.params.id let sort = req.query.sort
let db = await dbPromise if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) {
let playlist = await db.get('SELECT title FROM Playlist WHERE id = ?', id) sort = 'artist'
}
if (!playlist) return next(new Error('404 file not found')) let sortdir = req.query.sortdir
if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) {
sortdir = 'asc'
}
let tracks = await db.all('SELECT trackId FROM PlaylistEntry WHERE playlistId = ?', id) if (sort === 'id') {
playlist.tracks = tracks sort = 'ntry.indx'
} else {
sort = 'trck.' + sort
}
res.jsonp(playlist) res.jsonp(await playlist.getPlaylist(req.params.id, sort, sortdir))
}) })
// ----------- //
// FILE SERVER //
// ----------- //
router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => { router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
let id = req.params.id let id = req.params.id
let dl = (req.query.dl === '1') let dl = (req.query.dl === '1')
@ -210,7 +263,7 @@ router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
let track = await db.get('SELECT file FROM Track WHERE id = ?', id) let track = await db.get('SELECT file FROM Track WHERE id = ?', id)
if (!track) { if (!track) {
track = await lastfm.getTrackMetaReal(id) track = await lastfm.getTrackMetaReal(id)
if (!track) return next(new Error('404 file not found')) if (!track) throw new Error('404 file not found')
if (dl) { if (dl) {
lastfm.invokeDownload(id) lastfm.invokeDownload(id)
return res.end('<p>OK</p><script>window.close();</script>') return res.end('<p>OK</p><script>window.close();</script>')
@ -231,12 +284,17 @@ router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
res.redirect('/file/track' + fpath.substring(values.directory.length)) res.redirect('/file/track' + fpath.substring(values.directory.length))
}) })
// ---------- //
// ERROR SINK //
// ---------- //
router.use((err, req, res, next) => { router.use((err, req, res, next) => {
console.error(err) let msg = err.message
res.status(404).jsonp({error: 404}) dev && console.error(err.stack)
res.status(msg.indexOf('404') !== -1 ? 404 : 400).jsonp({ error: err.message, stack: dev ? err.stack.toString() : undefined })
}) })
app.use('/user', user(dbPromise, values.oauth, values.registrations === true)) app.use('/user', user(values.oauth, values.registrations === true))
app.use('/api', router) app.use('/api', router)
app.use('/file/track', express.static(path.resolve(values.directory))) app.use('/file/track', express.static(path.resolve(values.directory)))

View File

@ -2,10 +2,12 @@ import express from 'express'
import bcrypt from 'bcrypt' import bcrypt from 'bcrypt'
import { PromiseOAuth2 } from 'oauth-libre' import { PromiseOAuth2 } from 'oauth-libre'
import crypto from 'crypto' import crypto from 'crypto'
import { dbPromise } from './database'
const router = express.Router() const router = express.Router()
async function userInfoPublic (db, id) { async function userInfoPublic (id) {
let db = await dbPromise
let u = await db.get('SELECT id, username, image FROM User WHERE id = ?', id) let u = await db.get('SELECT id, username, image FROM User WHERE id = ?', id)
if (!u) return {} if (!u) return {}
return { return {
@ -15,7 +17,8 @@ async function userInfoPublic (db, id) {
} }
} }
export async function userInfo (db, id) { export async function userInfo (id) {
let db = await dbPromise
return db.get('SELECT * FROM User WHERE id = ?', id) return db.get('SELECT * FROM User WHERE id = ?', id)
} }
@ -24,14 +27,14 @@ export function userMiddleware (req, res, next) {
next() next()
} }
export function user (dbPromise, oauth, registrations) { export function user (oauth, registrations) {
router.get('/info', userMiddleware, async (req, res) => { router.get('/info', userMiddleware, async (req, res) => {
res.jsonp(await userInfoPublic(await dbPromise, req.session.user)) res.jsonp(await userInfoPublic(req.session.user))
}) })
router.get('/info/:id', userMiddleware, async (req, res) => { router.get('/info/:id', userMiddleware, async (req, res) => {
if (isNaN(parseInt(req.params.id))) throw new Error('Invalid user ID!') if (isNaN(parseInt(req.params.id))) throw new Error('Invalid user ID!')
res.jsonp(await userInfoPublic(await dbPromise, parseInt(req.params.id))) res.jsonp(await userInfoPublic(parseInt(req.params.id)))
}) })
if (!oauth) return router if (!oauth) return router