btrtracks/src/server.js

386 lines
11 KiB
JavaScript

import path from 'path'
import express from 'express'
import session from 'express-session'
import bodyParser from 'body-parser'
import connectSession from 'connect-redis'
import redis from 'redis'
import http from 'http'
import ffmpeg from 'fluent-ffmpeg'
import { user, userMiddleware } from './user'
import { dbPromise } from './database'
import * as asn from './common/async'
import * as playlist from './playlist'
import * as lastfm from './lastfm'
import * as external from './external.js'
require('express-async-errors')
const values = require(path.join(process.cwd(), 'values.json'))
const tracksPerPage = 100
const app = express()
const port = process.env.PORT || 3000
const dev = process.env.NODE_ENV === 'development'
const server = http.createServer(app)
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']
const srchcategories = ['title', 'artist', 'album']
const SessionStore = connectSession(session)
app.use(session({
key: values.session_key || 'Session',
secret: values.session_secret || 'ch4ng3 m3!',
store: new SessionStore({ client: redis.createClient(values.redis || { port: 6379 }) }),
resave: false,
saveUninitialized: true,
cookie: {
secure: !dev,
maxAge: 2678400000 // 1 month
}
}))
// ------ //
// TRACKS //
// ------ //
router.get('/tracks', userMiddleware, async (req, res) => {
let page = parseInt(req.query.page) || 1
if (isNaN(page)) {
page = 1
}
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'
}
const db = await dbPromise
const count = (await db.get('SELECT COUNT(*) as \'count\' FROM Track')).count
const pageCount = Math.ceil(count / tracksPerPage)
if (page > pageCount) page = pageCount
const offset = (page - 1) * tracksPerPage
const tracks = await db.all(`SELECT * FROM Track ORDER BY ${sort} ${sortdir} LIMIT ? OFFSET ?`, tracksPerPage, offset)
for (const i in tracks) {
delete tracks[i].file
}
res.jsonp({
page, count, pageCount, tracks
})
})
router.get('/tracks/search', userMiddleware, async (req, res) => {
const streamable = (req.query.streamable === '1')
let query = req.query.q
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) {
const ctr = query.split(':')
if (srchcategories.indexOf(ctr[0]) !== -1) {
qr = `ifnull(${ctr[0]}, '')`
query = query.substring(ctr[0].length + 1)
}
}
if (qr === '') {
for (const c in srchcategories) {
const cat = srchcategories[c]
if (parseInt(c) !== 0) qr += ' || '
qr += `ifnull(${cat}, '')`
}
}
if (query.indexOf('=') !== -1 && query.indexOf('\\=') === -1) {
query = query.replace('=', '')
exact = true
}
const original = String(query)
if (!exact) query = `%${query}%`
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'
}
// Paging
let page = parseInt(req.query.page) || 1
if (isNaN(page)) {
page = 1
}
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
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 {
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
}
} catch (e) {}
}
for (const i in tracks) {
delete tracks[i].file
}
res.jsonp({
page, count, pageCount, tracks
})
})
router.get('/track/:id', userMiddleware, async (req, res, next) => {
const id = req.params.id
const db = await dbPromise
let track = await db.get('SELECT * FROM Track WHERE id = ?', id)
if (!track) {
track = await external.getTrackMetaReal(id)
if (!track) throw new Error('404 track not found')
}
delete track.file
res.jsonp(track)
})
router.post('/track/:id', userMiddleware, async (req, res, next) => {
const id = req.params.id
const meta = req.body
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')
const m = await asn.setMetadata(track.file, meta)
await asn.updateDB(db, id, m.dbq)
res.jsonp(m)
})
// --------- //
// PLAYLISTS //
// --------- //
// General playlist endpoints
router.post('/playlist/new', userMiddleware, async (req, res, next) => {
if (!req.body.title) throw new Error('Title missing from body.')
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) => {
const 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) => {
const pId = req.params.playlistId
const 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) => {
const pId = req.params.playlistId
const 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) => {
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 })
})
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 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'
}
if (sort === 'id') {
sort = 'ntry.indx'
} else {
sort = 'trck.' + sort
}
res.jsonp(await playlist.getPlaylist(req.params.id, sort, sortdir))
})
// -------------- //
// 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) => {
const 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) => {
const token = req.query.token
if (!token) throw new Error('Failed to get token from LastFM!')
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) => {
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 external.getTrackMetaReal(id)
if (!track) throw new Error('404 file not found')
}
await scrobble(user, track)
res.jsonp({ success: true })
})
// ------------ //
// TRACK SERVER //
// ------------ //
router.get('/serve/by-id/:id', userMiddleware, async (req, res, next) => {
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 external.getTrackMetaReal(id)
if (!track) throw new Error('404 file not found')
if (dl) {
external.invokeDownload(id)
return res.end('<p>OK</p><script>window.close();</script>')
}
dev && console.log('Remote', track.file)
return ffmpeg(track.file)
.audioCodec('libmp3lame')
.format('mp3')
.on('error', (e) => console.error(e))
.pipe(res, { end: true })
}
const fpath = path.resolve(track.file)
res.set('Cache-Control', 'public, max-age=31557600')
if (dl) return res.download(fpath)
res.redirect('/file/track' + fpath.substring(values.directory.length))
})
// ---------- //
// ERROR SINK //
// ---------- //
router.use((err, req, res, next) => {
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 })
})
app.use('/user', user(values.oauth, values.registrations === true))
app.use('/api', router)
app.use('/file/track', express.static(path.resolve(values.directory)))
app.use('/', express.static(path.join(process.cwd(), 'public')))
const host = process.env.NODE_ENV === 'development' ? '0.0.0.0' : '127.0.0.1'
server.listen(port, host, async function () {
const db = await dbPromise
await db.migrate()
console.log(`app running on port ${port}`)
})