2018-10-05 10:36:57 +00:00
|
|
|
import path from 'path'
|
|
|
|
import express from 'express'
|
2019-06-16 09:28:46 +00:00
|
|
|
import session from 'express-session'
|
2019-12-17 17:52:18 +00:00
|
|
|
import bodyParser from 'body-parser'
|
|
|
|
import connectSession from 'connect-redis'
|
|
|
|
import redis from 'redis'
|
2019-01-25 21:11:58 +00:00
|
|
|
import http from 'http'
|
2019-01-25 15:00:35 +00:00
|
|
|
import https from 'https'
|
|
|
|
import ffmpeg from 'fluent-ffmpeg'
|
2019-06-16 09:28:46 +00:00
|
|
|
import { user, userMiddleware } from './user'
|
2019-12-17 17:52:18 +00:00
|
|
|
import { dbPromise } from './database'
|
2019-01-25 15:00:35 +00:00
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
import playlist from './playlist'
|
|
|
|
import lastfm from './lastfm'
|
2018-10-05 10:36:57 +00:00
|
|
|
|
|
|
|
require('express-async-errors')
|
|
|
|
|
2018-10-18 15:34:47 +00:00
|
|
|
const values = require(path.join(process.cwd(), 'values.json'))
|
2018-10-05 10:36:57 +00:00
|
|
|
const tracksPerPage = 100
|
|
|
|
|
|
|
|
const app = express()
|
|
|
|
const port = process.env.PORT || 3000
|
|
|
|
|
2019-01-25 21:11:58 +00:00
|
|
|
const dev = process.env.NODE_ENV === 'development'
|
|
|
|
const server = http.createServer(app)
|
|
|
|
|
|
|
|
if (dev) {
|
|
|
|
const morgan = require('morgan')
|
|
|
|
app.use(morgan('dev'))
|
|
|
|
}
|
|
|
|
|
2019-06-16 10:12:40 +00:00
|
|
|
app.set('trust proxy', 1)
|
2019-12-17 17:52:18 +00:00
|
|
|
app.use(bodyParser.urlencoded({ extended: false }))
|
|
|
|
app.use(bodyParser.json())
|
2019-06-16 10:12:40 +00:00
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
const router = express.Router()
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year']
|
2019-01-19 16:27:12 +00:00
|
|
|
const srchcategories = ['title', 'artist', 'album']
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
let SessionStore = connectSession(session)
|
2019-06-16 09:28:46 +00:00
|
|
|
app.use(session({
|
|
|
|
key: values.session_key || 'Session',
|
|
|
|
secret: values.session_secret || 'ch4ng3 m3!',
|
2019-12-17 17:52:18 +00:00
|
|
|
store: new SessionStore({ client: redis.createClient(values.redis || { port: 6379 }) }),
|
2019-06-16 09:28:46 +00:00
|
|
|
resave: false,
|
|
|
|
saveUninitialized: true,
|
|
|
|
cookie: {
|
2019-06-16 10:12:40 +00:00
|
|
|
secure: !dev,
|
2019-06-16 09:28:46 +00:00
|
|
|
maxAge: 2678400000 // 1 month
|
|
|
|
}
|
|
|
|
}))
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
// ------ //
|
|
|
|
// TRACKS //
|
|
|
|
// ------ //
|
|
|
|
|
2019-06-16 09:28:46 +00:00
|
|
|
router.get('/tracks', userMiddleware, async (req, res) => {
|
2018-10-05 10:36:57 +00:00
|
|
|
let page = parseInt(req.query.page) || 1
|
|
|
|
if (isNaN(page)) {
|
|
|
|
page = 1
|
|
|
|
}
|
|
|
|
|
2019-01-19 16:27:12 +00:00
|
|
|
let sort = req.query.sort
|
|
|
|
if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) {
|
|
|
|
sort = 'artist'
|
|
|
|
}
|
|
|
|
|
|
|
|
let sortdir = req.query.sortdir
|
|
|
|
if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) {
|
|
|
|
sortdir = 'asc'
|
|
|
|
}
|
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
let db = await dbPromise
|
|
|
|
let count = (await db.get('SELECT COUNT(*) FROM Track'))['COUNT(*)']
|
|
|
|
|
|
|
|
let pageCount = Math.ceil(count / tracksPerPage)
|
|
|
|
|
|
|
|
if (page > pageCount) page = pageCount
|
|
|
|
|
|
|
|
let offset = (page - 1) * tracksPerPage
|
2019-01-19 16:27:12 +00:00
|
|
|
let tracks = await db.all(`SELECT * FROM Track ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`, tracksPerPage, offset)
|
2018-10-05 10:36:57 +00:00
|
|
|
|
|
|
|
for (let i in tracks) {
|
|
|
|
delete tracks[i].file
|
|
|
|
}
|
|
|
|
|
|
|
|
res.jsonp({
|
|
|
|
page, count, pageCount, tracks
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2019-06-16 09:28:46 +00:00
|
|
|
router.get('/tracks/search', userMiddleware, async (req, res) => {
|
2018-10-09 15:15:26 +00:00
|
|
|
let query = req.query.q
|
2019-01-25 15:00:35 +00:00
|
|
|
let streamable = (req.query.streamable === '1')
|
2018-10-05 14:10:05 +00:00
|
|
|
let qr = ''
|
|
|
|
let exact = false
|
|
|
|
|
|
|
|
if (query.indexOf(':') !== -1) {
|
|
|
|
let ctr = query.split(':')
|
|
|
|
|
|
|
|
if (srchcategories.indexOf(ctr[0]) !== -1) {
|
|
|
|
qr = `ifnull(${ctr[0]}, '')`
|
|
|
|
query = query.substring(ctr[0].length + 1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (qr === '') {
|
|
|
|
for (let c in srchcategories) {
|
|
|
|
let cat = srchcategories[c]
|
|
|
|
if (parseInt(c) !== 0) qr += ' || '
|
|
|
|
qr += `ifnull(${cat}, '')`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (query.indexOf('=') !== -1 && query.indexOf('\\=') === -1) {
|
|
|
|
query = query.replace('=', '')
|
|
|
|
exact = true
|
|
|
|
}
|
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
let original = String(query)
|
2018-10-05 14:10:05 +00:00
|
|
|
if (!exact) query = `%${query}%`
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2019-01-19 16:27:12 +00:00
|
|
|
let sort = req.query.sort
|
|
|
|
if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) {
|
|
|
|
sort = 'artist'
|
|
|
|
}
|
|
|
|
|
|
|
|
let sortdir = req.query.sortdir
|
|
|
|
if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) {
|
|
|
|
sortdir = 'asc'
|
|
|
|
}
|
|
|
|
|
2018-10-09 15:15:26 +00:00
|
|
|
// Paging
|
|
|
|
let page = parseInt(req.query.page) || 1
|
|
|
|
if (isNaN(page)) {
|
|
|
|
page = 1
|
|
|
|
}
|
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
let db = await dbPromise
|
2018-10-09 15:15:26 +00:00
|
|
|
let count = (await db.get(`SELECT COUNT(*) FROM Track WHERE ${qr} LIKE ?`, query))['COUNT(*)']
|
|
|
|
let pageCount = Math.ceil(count / tracksPerPage)
|
|
|
|
|
|
|
|
if (page > pageCount) page = pageCount
|
|
|
|
|
|
|
|
let offset = (page - 1) * tracksPerPage
|
2019-01-19 16:27:12 +00:00
|
|
|
let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`,
|
|
|
|
query, tracksPerPage, offset)
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
let llimit = tracksPerPage - count
|
|
|
|
if (streamable && page === pageCount && llimit > 1) {
|
|
|
|
if (llimit < 10) llimit = 10
|
|
|
|
try {
|
|
|
|
let lfm = await lastfm.search(original, llimit)
|
|
|
|
if (lfm && lfm.length) {
|
|
|
|
tracks = tracks.concat(lfm)
|
2019-01-25 21:11:58 +00:00
|
|
|
count = count + lfm.length
|
|
|
|
if (page == 0) page = 1
|
|
|
|
if (pageCount == 0) pageCount = 1
|
2019-01-25 15:00:35 +00:00
|
|
|
}
|
|
|
|
} catch (e) {}
|
|
|
|
}
|
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
for (let i in tracks) {
|
|
|
|
delete tracks[i].file
|
|
|
|
}
|
|
|
|
|
2018-10-09 15:15:26 +00:00
|
|
|
res.jsonp({
|
|
|
|
page, count, pageCount, tracks
|
|
|
|
})
|
2018-10-05 10:36:57 +00:00
|
|
|
})
|
|
|
|
|
2019-06-16 09:28:46 +00:00
|
|
|
router.get('/track/:id', userMiddleware, async (req, res, next) => {
|
2018-10-05 10:36:57 +00:00
|
|
|
let id = req.params.id
|
2019-01-25 15:00:35 +00:00
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
let db = await dbPromise
|
|
|
|
let track = await db.get('SELECT * FROM Track WHERE id = ?', id)
|
2019-01-25 15:00:35 +00:00
|
|
|
if (!track) {
|
|
|
|
track = await lastfm.getTrackMetaReal(id)
|
2019-12-17 17:52:18 +00:00
|
|
|
if (!track) throw new Error('404 track not found')
|
2019-01-25 15:00:35 +00:00
|
|
|
}
|
2018-10-05 10:36:57 +00:00
|
|
|
|
|
|
|
delete track.file
|
|
|
|
|
|
|
|
res.jsonp(track)
|
|
|
|
})
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
// --------- //
|
|
|
|
// PLAYLISTS //
|
|
|
|
// --------- //
|
2018-10-10 10:02:56 +00:00
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
// 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))
|
2018-10-05 10:36:57 +00:00
|
|
|
})
|
|
|
|
|
2019-06-16 09:28:46 +00:00
|
|
|
router.get('/playlist/:id', userMiddleware, async (req, res, next) => {
|
2019-12-17 17:52:18 +00:00
|
|
|
let sort = req.query.sort
|
|
|
|
if (!sort || sortfields.indexOf(sort.toLowerCase()) === -1) {
|
|
|
|
sort = 'artist'
|
|
|
|
}
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
let sortdir = req.query.sortdir
|
|
|
|
if (!sortdir || (sortdir !== 'desc' && sortdir !== 'asc')) {
|
|
|
|
sortdir = 'asc'
|
|
|
|
}
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
if (sort === 'id') {
|
|
|
|
sort = 'ntry.indx'
|
|
|
|
} else {
|
|
|
|
sort = 'trck.' + sort
|
|
|
|
}
|
2018-10-10 10:02:56 +00:00
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
res.jsonp(await playlist.getPlaylist(req.params.id, sort, sortdir))
|
2018-10-05 10:36:57 +00:00
|
|
|
})
|
|
|
|
|
2019-12-18 12:57:02 +00:00
|
|
|
// -------------- //
|
|
|
|
// LASTFM CONNECT //
|
|
|
|
// -------------- //
|
|
|
|
|
|
|
|
async function scrobble (user, track) {
|
|
|
|
if (await lastfm.getSessionForUser(user)) {
|
|
|
|
try {
|
|
|
|
await lastfm.scrobbleTrack(user, track)
|
|
|
|
} catch (e) {
|
|
|
|
console.warn('Track scrobbling failed for', user)
|
|
|
|
console.error(e.stack)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
router.get('/lastfm', userMiddleware, async (req, res, next) => {
|
|
|
|
let sess = await lastfm.getSessionForUser(req.session.user)
|
|
|
|
if (sess) return res.jsonp({ connected: true, name: sess.name })
|
|
|
|
res.jsonp({ connected: false })
|
|
|
|
})
|
|
|
|
|
|
|
|
router.get('/lastfm/connect', userMiddleware, async (req, res, next) => {
|
|
|
|
if (await lastfm.getSessionForUser(req.session.user)) throw new Error('You already have a LFM session!')
|
|
|
|
res.redirect(lastfm.getAuthURL())
|
|
|
|
})
|
|
|
|
|
|
|
|
router.get('/lastfm/disconnect', userMiddleware, async (req, res, next) => {
|
|
|
|
await lastfm.disregardSession(req.session.user)
|
|
|
|
res.redirect('/')
|
|
|
|
})
|
|
|
|
|
|
|
|
router.get('/lastfm/_redirect', userMiddleware, async (req, res, next) => {
|
|
|
|
let token = req.query.token
|
|
|
|
if (!token) throw new Error('Failed to get token from LastFM!')
|
|
|
|
let 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
|
|
|
|
let track = await db.get('SELECT title,artist,album,duration FROM Track WHERE id = ?', id)
|
|
|
|
if (!track) {
|
|
|
|
track = await lastfm.getTrackMetaReal(id)
|
|
|
|
if (!track) throw new Error('404 file not found')
|
|
|
|
}
|
|
|
|
await scrobble(user, track)
|
|
|
|
res.jsonp({ success: true })
|
|
|
|
})
|
|
|
|
|
|
|
|
// ------------ //
|
|
|
|
// TRACK SERVER //
|
|
|
|
// ------------ //
|
2019-12-17 17:52:18 +00:00
|
|
|
|
2019-06-16 09:28:46 +00:00
|
|
|
router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
|
2018-10-05 10:36:57 +00:00
|
|
|
let id = req.params.id
|
2019-01-25 15:00:35 +00:00
|
|
|
let dl = (req.query.dl === '1')
|
2018-10-05 10:36:57 +00:00
|
|
|
let db = await dbPromise
|
|
|
|
let track = await db.get('SELECT file FROM Track WHERE id = ?', id)
|
2019-01-25 15:00:35 +00:00
|
|
|
if (!track) {
|
|
|
|
track = await lastfm.getTrackMetaReal(id)
|
2019-12-17 17:52:18 +00:00
|
|
|
if (!track) throw new Error('404 file not found')
|
2019-01-26 22:35:09 +00:00
|
|
|
if (dl) {
|
|
|
|
lastfm.invokeDownload(id)
|
|
|
|
return res.end('<p>OK</p><script>window.close();</script>')
|
|
|
|
}
|
2019-12-16 19:24:44 +00:00
|
|
|
dev && console.log("Remote", track.file)
|
2019-01-25 15:00:35 +00:00
|
|
|
return ffmpeg(track.file)
|
|
|
|
.audioCodec('libmp3lame')
|
|
|
|
.format('mp3')
|
|
|
|
.on('error', (e) => console.error(e))
|
|
|
|
.pipe(res, {end: true})
|
|
|
|
}
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2018-10-10 10:02:56 +00:00
|
|
|
let fpath = path.resolve(track.file)
|
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
res.set('Cache-Control', 'public, max-age=31557600')
|
2019-01-25 15:00:35 +00:00
|
|
|
if (dl) return res.download(fpath)
|
2018-10-10 10:02:56 +00:00
|
|
|
|
2018-10-11 08:41:26 +00:00
|
|
|
res.redirect('/file/track' + fpath.substring(values.directory.length))
|
2018-10-05 10:36:57 +00:00
|
|
|
})
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
// ---------- //
|
|
|
|
// ERROR SINK //
|
|
|
|
// ---------- //
|
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
router.use((err, req, res, next) => {
|
2019-12-17 17:52:18 +00:00
|
|
|
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 })
|
2018-10-05 10:36:57 +00:00
|
|
|
})
|
|
|
|
|
2019-12-17 17:52:18 +00:00
|
|
|
app.use('/user', user(values.oauth, values.registrations === true))
|
2019-06-16 09:28:46 +00:00
|
|
|
|
2018-10-05 10:36:57 +00:00
|
|
|
app.use('/api', router)
|
2018-10-11 08:41:26 +00:00
|
|
|
app.use('/file/track', express.static(path.resolve(values.directory)))
|
2018-10-18 15:34:47 +00:00
|
|
|
app.use('/', express.static(path.join(process.cwd(), 'public')))
|
2018-10-05 10:36:57 +00:00
|
|
|
|
2019-01-15 14:21:33 +00:00
|
|
|
const host = process.env.NODE_ENV === 'development' ? '0.0.0.0' : '127.0.0.1'
|
|
|
|
|
2019-01-25 21:11:58 +00:00
|
|
|
server.listen(port, host, function () {
|
2018-10-05 10:36:57 +00:00
|
|
|
console.log(`app running on port ${port}`)
|
|
|
|
})
|