Simple playlists, download automatic db insert
This commit is contained in:
parent
5b5414a189
commit
a04bd3e69b
@ -21,6 +21,7 @@ CREATE TABLE PlaylistEntry (
|
||||
id INTEGER PRIMARY KEY,
|
||||
playlistId INTEGER,
|
||||
trackId INTEGER,
|
||||
indx INTEGER,
|
||||
CONSTRAINT PE_fk_playlistId FOREIGN KEY (playlistId)
|
||||
REFERENCES Playlist (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT PE_fk_trackId FOREIGN KEY (trackId)
|
||||
|
18
package.json
18
package.json
@ -13,16 +13,18 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"bcrypt": "^3.0.7",
|
||||
"bluebird": "^3.5.2",
|
||||
"connect-redis": "^3.4.1",
|
||||
"express": "^4.16.3",
|
||||
"express-async-errors": "^3.0.0",
|
||||
"express-session": "^1.16.2",
|
||||
"bluebird": "^3.7.2",
|
||||
"body-parser": "^1.19.0",
|
||||
"connect-redis": "^3.4.2",
|
||||
"express": "^4.17.1",
|
||||
"express-async-errors": "^3.1.1",
|
||||
"express-session": "^1.17.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"fs-extra": "^7.0.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"oauth-libre": "^0.9.17",
|
||||
"socket.io": "^2.1.1",
|
||||
"sqlite": "^3.0.2",
|
||||
"redis": "^2.8.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"sqlite": "^3.0.3",
|
||||
"sqlite3": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -177,7 +177,7 @@ tr.external td {
|
||||
width: 0%;
|
||||
background-color: #00b7ff;
|
||||
}
|
||||
.ctx-menu {
|
||||
.ctx-menu, .ctx-sub-items {
|
||||
position: absolute;
|
||||
background-color: #0c2233;
|
||||
border: 2px solid #031421;
|
||||
@ -200,6 +200,17 @@ tr.external td {
|
||||
.ctx-item:hover, .dropdown-content div:hover {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -210,6 +221,9 @@ tr.external td {
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.player .inline-flex {
|
||||
overflow: hidden;
|
||||
}
|
||||
#search-clear {
|
||||
display: none;
|
||||
}
|
||||
|
@ -72,6 +72,10 @@
|
||||
<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="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>
|
||||
</div>
|
||||
<div class="sidebar background" id="options-drop">
|
||||
|
226
public/index.js
226
public/index.js
@ -38,7 +38,11 @@
|
||||
|
||||
// Playlists
|
||||
var playlist = null
|
||||
var playlistCache = {}
|
||||
var playingList = null
|
||||
var pSel = document.getElementById('playlist-select')
|
||||
var dropdownBtn = pSel.querySelector('.dropdown-button')
|
||||
var ctxPlaylist = document.getElementById('ctx-playlists')
|
||||
|
||||
// Options
|
||||
var options = {
|
||||
@ -108,22 +112,38 @@
|
||||
return array
|
||||
}
|
||||
|
||||
function httpGet (url, callback) {
|
||||
function _httpRequest (method, url, data) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
var xmlHttp = new XMLHttpRequest()
|
||||
xmlHttp.onreadystatechange = function () {
|
||||
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
|
||||
resolve(JSON.parse(xmlHttp.responseText))
|
||||
} 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.open('GET', url, true)
|
||||
xmlHttp.send(null)
|
||||
xmlHttp.open(method, url, true)
|
||||
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) {
|
||||
if (!artist) return title
|
||||
return artist + ' - ' + title
|
||||
@ -217,7 +237,7 @@
|
||||
}
|
||||
|
||||
function updateQT (q) {
|
||||
if (playlist === 0) showTracks(pageNum)
|
||||
if (playlist === -1) showTracks(pageNum)
|
||||
if (q) {
|
||||
qTag.style.display = 'block'
|
||||
var cnt = '+' + queue.length
|
||||
@ -228,6 +248,25 @@
|
||||
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) {
|
||||
if (ctxState === 0) return
|
||||
let dt = el.getAttribute("data-action")
|
||||
@ -236,7 +275,7 @@
|
||||
play(ctxState)
|
||||
break
|
||||
case 'queue':
|
||||
if (playlist === 0)
|
||||
if (playlist === -1)
|
||||
removeFromQueue(ctxState)
|
||||
else
|
||||
queue.push(ctxState)
|
||||
@ -244,6 +283,17 @@
|
||||
case 'download':
|
||||
window.open('/api/serve/by-id/' + ctxState + '?dl=1', '_blank')
|
||||
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()
|
||||
updateQT(qTag.style.display === 'block')
|
||||
@ -256,6 +306,12 @@
|
||||
let qbtn = menu.querySelector('.ctx-item[data-action="queue"]')
|
||||
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
|
||||
}
|
||||
|
||||
@ -275,7 +331,7 @@
|
||||
}
|
||||
|
||||
function handleHeads () {
|
||||
if (playlist === 0) return
|
||||
if (playlist === -1) return
|
||||
let els = elementsToArray(document.querySelectorAll('th'))
|
||||
let sortarr = document.createElement('span')
|
||||
sortarr.className = 'order' + (options.sortdir === 'desc' ? ' dn' : '')
|
||||
@ -291,11 +347,13 @@
|
||||
if (options.sortby !== sortattr) {
|
||||
options.sortdir = 'asc'
|
||||
options.sortby = sortattr
|
||||
return showTracks(pageNum)
|
||||
if (playlist == null || playlist < 0) return showTracks(pageNum)
|
||||
showPlaylist(playlist)
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
tag.addEventListener('click', function (e) {
|
||||
play(track.id)
|
||||
play(track.trackId || track.id)
|
||||
}, false)
|
||||
|
||||
tag.addEventListener('contextmenu', function (e) {
|
||||
e.preventDefault()
|
||||
ctxTrack(track.id, e)
|
||||
ctxTrack(track.trackId || track.id, e)
|
||||
}, false)
|
||||
|
||||
table.appendChild(tag)
|
||||
@ -324,7 +382,7 @@
|
||||
|
||||
function recursionQueueList (index) {
|
||||
let qid = queue[index]
|
||||
if (!qid || playlist !== 0) return
|
||||
if (!qid || playlist !== -1) return
|
||||
|
||||
httpGet('/api/track/' + qid).then(function (data) {
|
||||
let tag = trackDataRow(data)
|
||||
@ -358,6 +416,76 @@
|
||||
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) {
|
||||
let query = input.value
|
||||
if (searching && query.trim() === '') {
|
||||
@ -370,10 +498,13 @@
|
||||
if (page > pages) page = pages
|
||||
updatePaging()
|
||||
|
||||
if (playlist === 0) {
|
||||
if (playlist === -1) {
|
||||
return constructQueue()
|
||||
}
|
||||
|
||||
playlist = null
|
||||
dropdownBtn.innerHTML = 'All Tracks'
|
||||
|
||||
if (query.trim() === '') {
|
||||
return httpGet('/api/tracks?page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) {
|
||||
pageNum = page
|
||||
@ -399,7 +530,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
function play (id, q) {
|
||||
function play (id, q, fromNext) {
|
||||
if (id < 1) return playNext()
|
||||
httpGet('/api/track/' + id).then(function (data) {
|
||||
let title = shortTitle(data.title, data.artist)
|
||||
@ -415,19 +546,57 @@
|
||||
updateQT(q)
|
||||
|
||||
nowPlaying = id
|
||||
if (!fromNext) {
|
||||
if (playlist != null && playlist > -1 && !q) {
|
||||
playingList = playlist
|
||||
} else {
|
||||
playingList = null
|
||||
}
|
||||
}
|
||||
}, function (e) {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
|
||||
function playNext () {
|
||||
if (queue.length !== 0) return play(queue.shift(), true)
|
||||
function playPrevious () {
|
||||
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
|
||||
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
|
||||
@ -441,33 +610,37 @@
|
||||
}
|
||||
|
||||
function handleSelect () {
|
||||
let btn = pSel.querySelector('.dropdown-button')
|
||||
let btns = elementsToArray(pSel.querySelectorAll('.dropdown-content div'))
|
||||
|
||||
for (let i in btns) {
|
||||
let btni = btns[i]
|
||||
let cnt = btni.getAttribute('data-value')
|
||||
if (btni.getAttribute('data-bound')) continue
|
||||
|
||||
btni.addEventListener('click', function (e) {
|
||||
let last = btn.innerHTML
|
||||
let isPlaylist = false
|
||||
let last = dropdownBtn.innerHTML
|
||||
let set = btni.innerHTML
|
||||
switch (cnt) {
|
||||
case 'all':
|
||||
playlist = null
|
||||
break
|
||||
case 'queue':
|
||||
playlist = 0
|
||||
playlist = -1
|
||||
break
|
||||
case 'options':
|
||||
toggleOptions()
|
||||
set = last
|
||||
break
|
||||
default:
|
||||
isPlaylist = true
|
||||
showPlaylist(cnt)
|
||||
}
|
||||
btn.innerHTML = set
|
||||
showTracks(pageNum)
|
||||
dropdownBtn.innerHTML = set
|
||||
if (!isPlaylist) showTracks(pageNum)
|
||||
}, false)
|
||||
|
||||
btni.setAttribute('data-bound', true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,9 +705,7 @@
|
||||
|
||||
document.getElementById('player-next').addEventListener('click', playNext, false)
|
||||
|
||||
document.getElementById('player-prev').addEventListener('click', function (e) {
|
||||
play(nowPlaying - 1)
|
||||
}, false)
|
||||
document.getElementById('player-prev').addEventListener('click', playPrevious, false)
|
||||
|
||||
document.getElementById('jump-first').addEventListener('click', function (e) {
|
||||
showTracks(1)
|
||||
@ -600,6 +771,7 @@
|
||||
loadOptions()
|
||||
showTracks(1)
|
||||
handleHash(window.location.hash)
|
||||
populatePlaylists()
|
||||
handleSelect()
|
||||
handleOptions()
|
||||
})()
|
||||
|
11
src/database.js
Normal file
11
src/database.js
Normal 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 }
|
@ -1,22 +1,17 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import sqlite from 'sqlite'
|
||||
import Promise from 'bluebird'
|
||||
import readline from 'readline'
|
||||
|
||||
import asn from './common/async'
|
||||
import dl from './common/download'
|
||||
import { dbPromise } from './database'
|
||||
|
||||
const values = require(path.join(process.cwd(), 'values.json'))
|
||||
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"
|
||||
|
||||
async function interactive (fpath, db) {
|
||||
async function interactive (fpath) {
|
||||
let rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
|
@ -6,6 +6,7 @@ import path from 'path'
|
||||
|
||||
import asn from './common/async'
|
||||
import dl from './common/download'
|
||||
import { dbPromise } from './database'
|
||||
|
||||
const values = require(path.join(process.cwd(), 'values.json'))
|
||||
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 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.')
|
||||
rl.close()
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ import path from 'path'
|
||||
import asn from './common/async'
|
||||
import dl from './common/download'
|
||||
import crypto from 'crypto'
|
||||
import { dbPromise } from './database'
|
||||
|
||||
const fs = require('fs').promises
|
||||
const values = require(path.join(process.cwd(), 'values.json'))
|
||||
@ -10,7 +11,6 @@ const memexpire = 1800
|
||||
let externalTracks = {}
|
||||
let downloadQueue = []
|
||||
let downloading = false
|
||||
let dbPromise
|
||||
|
||||
function createHash (data) {
|
||||
return crypto
|
||||
@ -154,7 +154,4 @@ function invokeDownload (add) {
|
||||
}, 2 * 1000)
|
||||
}
|
||||
|
||||
export default function (db) {
|
||||
dbPromise = db
|
||||
return { search, getTrackMetaReal, invokeDownload }
|
||||
}
|
||||
export default { search, getTrackMetaReal, invokeDownload }
|
||||
|
102
src/playlist.js
Normal file
102
src/playlist.js
Normal 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 }
|
116
src/server.js
116
src/server.js
@ -1,50 +1,48 @@
|
||||
import path from 'path'
|
||||
import sqlite from 'sqlite'
|
||||
import Promise from 'bluebird'
|
||||
import express from 'express'
|
||||
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 https from 'https'
|
||||
import ffmpeg from 'fluent-ffmpeg'
|
||||
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')
|
||||
|
||||
const values = require(path.join(process.cwd(), 'values.json'))
|
||||
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 port = process.env.PORT || 3000
|
||||
|
||||
const dev = process.env.NODE_ENV === 'development'
|
||||
const server = http.createServer(app)
|
||||
|
||||
const lastfm = lfmda(dbPromise)
|
||||
|
||||
if (dev) {
|
||||
const morgan = require('morgan')
|
||||
app.use(morgan('dev'))
|
||||
}
|
||||
|
||||
app.set('trust proxy', 1)
|
||||
app.use(bodyParser.urlencoded({ extended: false }))
|
||||
app.use(bodyParser.json())
|
||||
|
||||
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']
|
||||
|
||||
let SessionStore = redis(session)
|
||||
let SessionStore = connectSession(session)
|
||||
app.use(session({
|
||||
key: values.session_key || 'Session',
|
||||
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,
|
||||
saveUninitialized: true,
|
||||
cookie: {
|
||||
@ -53,6 +51,10 @@ app.use(session({
|
||||
}
|
||||
}))
|
||||
|
||||
// ------ //
|
||||
// TRACKS //
|
||||
// ------ //
|
||||
|
||||
router.get('/tracks', userMiddleware, async (req, res) => {
|
||||
let page = parseInt(req.query.page) || 1
|
||||
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)
|
||||
if (!track) {
|
||||
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
|
||||
@ -183,26 +185,77 @@ router.get('/track/:id', userMiddleware, async (req, res, next) => {
|
||||
res.jsonp(track)
|
||||
})
|
||||
|
||||
router.get('/playlists', userMiddleware, async (req, res, next) => {
|
||||
let db = await dbPromise
|
||||
let playlists = await db.all('SELECT * FROM Playlist WHERE userId = ? OR userId = NULL', req.session.user)
|
||||
// --------- //
|
||||
// PLAYLISTS //
|
||||
// --------- //
|
||||
|
||||
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) => {
|
||||
let id = req.params.id
|
||||
let db = await dbPromise
|
||||
let playlist = await db.get('SELECT title FROM Playlist WHERE id = ?', id)
|
||||
let sort = req.query.sort
|
||||
if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) {
|
||||
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)
|
||||
playlist.tracks = tracks
|
||||
if (sort === 'id') {
|
||||
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) => {
|
||||
let id = req.params.id
|
||||
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)
|
||||
if (!track) {
|
||||
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) {
|
||||
lastfm.invokeDownload(id)
|
||||
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))
|
||||
})
|
||||
|
||||
// ---------- //
|
||||
// ERROR SINK //
|
||||
// ---------- //
|
||||
|
||||
router.use((err, req, res, next) => {
|
||||
console.error(err)
|
||||
res.status(404).jsonp({error: 404})
|
||||
let 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 })
|
||||
})
|
||||
|
||||
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('/file/track', express.static(path.resolve(values.directory)))
|
||||
|
13
src/user.js
13
src/user.js
@ -2,10 +2,12 @@ import express from 'express'
|
||||
import bcrypt from 'bcrypt'
|
||||
import { PromiseOAuth2 } from 'oauth-libre'
|
||||
import crypto from 'crypto'
|
||||
import { dbPromise } from './database'
|
||||
|
||||
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)
|
||||
if (!u) 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)
|
||||
}
|
||||
|
||||
@ -24,14 +27,14 @@ export function userMiddleware (req, res, next) {
|
||||
next()
|
||||
}
|
||||
|
||||
export function user (dbPromise, oauth, registrations) {
|
||||
export function user (oauth, registrations) {
|
||||
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) => {
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user