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('

OK

') } 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}`) })