diff --git a/package.json b/package.json index 0b7f9f6..5aa0dc4 100644 --- a/package.json +++ b/package.json @@ -19,10 +19,12 @@ "fs-extra": "^7.0.0", "socket.io": "^2.1.1", "sqlite": "^3.0.0", - "sqlite3": "^4.0.3" + "sqlite3": "^4.0.3", + "ws": "^6.1.3" }, "devDependencies": { "babel-core": "^6.26.3", - "babel-env": "^2.4.1" + "babel-env": "^2.4.1", + "morgan": "^1.9.1" } } diff --git a/public/index.css b/public/index.css index ba293e8..9b6198b 100644 --- a/public/index.css +++ b/public/index.css @@ -325,9 +325,19 @@ canvas#visualizer { padding: 10px; display: flex; } -.sidebar .option.checkbox { +.sidebar .separator { + font-weight: bold; + padding: 10px; + border-bottom: 2px solid #004561; + font-size: 1.2em; +} +.sidebar .option.checkbox, .sidebar .option.action { flex-direction: row; } +.sidebar .option.action button { + flex-grow: 1; + margin: 10px; +} .sidebar .option label { flex-grow: 1; } diff --git a/public/index.html b/public/index.html index a740724..d82d14b 100644 --- a/public/index.html +++ b/public/index.html @@ -79,14 +79,17 @@ diff --git a/public/index.js b/public/index.js index 241602c..e7d969b 100644 --- a/public/index.js +++ b/public/index.js @@ -17,6 +17,7 @@ Year \ Duration \ ' + var nowPlaying = 0 var externalStream = false @@ -45,6 +46,8 @@ streamable: true, sortby: 'id', sortdir: 'asc', + device: '', + remote: false } window.mobilecheck = function() { diff --git a/src/lastfm.js b/src/lastfm.js index 2d936e5..e637e4b 100644 --- a/src/lastfm.js +++ b/src/lastfm.js @@ -8,14 +8,17 @@ const values = require(path.join(process.cwd(), 'values.json')) let externalTracks = {} function createHash (data) { - return crypto.createHash('sha1').update(data.artist + data.name).digest('hex') + return crypto + .createHash('sha1') + .update(data.artist + data.name) + .digest('hex') .substr(0, 8) } async function getTrackMetaReal (id) { if (!id || !externalTracks[id]) return null let trdata = externalTracks[id] - if (trdata.file) return trdata + if (trdata.file) return Object.assign({}, trdata) let trsrch = 'ytsearch1:' + trdata.artist + ' - ' + trdata.title let dldata = await dl.getVideoInfo(trsrch) @@ -29,31 +32,45 @@ async function getTrackMetaReal (id) { external: true } - return externalTracks[id] + return Object.assign({}, externalTracks[id]) } async function search (track, limit = 30) { if (!values.lastfm) return [] + let data try { data = await as.GET(`http://ws.audioscrobbler.com/2.0/?method=track.search&track=${track}&api_key=${values.lastfm}&format=json&limit=${limit}`) data = JSON.parse(data) + + if (!data.results || !data.results.trackmatches || !data.results.trackmatches.track) { + throw new Error('No results') + } } catch (e) { - console.warn(e.stack) return [] } - if (!data.results || !data.results.trackmatches || !data.results.trackmatches.track) return + let final = [] for (let i in data.results.trackmatches.track) { let res = data.results.trackmatches.track[i] - try { - let tr = { artist: res.artist, title: res.name, external: true, id: createHash(res) } - externalTracks[tr.id] = tr - final.push(tr) - } catch (e) { - continue + let clean = { + id: createHash(res), + artist: res.artist, + title: res.name, + external: true } + + if (externalTracks[clean.id]) { + // Copy object + clean = Object.assign({}, externalTracks[clean.id]) + } else { + // Save in cache + externalTracks[clean.id] = clean + } + + final.push(clean) } + return final } diff --git a/src/remote-control.js b/src/remote-control.js new file mode 100644 index 0000000..31494d4 --- /dev/null +++ b/src/remote-control.js @@ -0,0 +1,75 @@ +import WebSocket from 'ws' + +const wss = new WebSocket.Server({ noServer: true }) + +function getClientByDevice (dev) { + for (let i in wss.clients) { + let client = wss.clients[i] + if (client.device === dev) return client + } + return null +} + +wss.on('connection', function (ws) { + ws.on('device', function (devid) { + ws.device = devid + }) + + ws.on('connect', function (devid) { + let target = getClientByDevice(devid) + if (!target) return ws.emit('errno', { code: 100, error: 'Target device is not available.' }) + target.emit('request', ws.device) + }) + + ws.on('detach', function () { + if (!ws.controlled) return + ws.controlled = null + let target = getClientByDevice(devid) + if (!target) return + target.controller = null + target.emit('detach') + }) + + ws.on('accept', function (devid) { + ws.controller = devid + let target = getClientByDevice(devid) + if (!target) return ws.emit('errno', { code: 100, error: 'Target device is not available.' }) + target.controlled = devid + target.emit('accepted', devid) + }) + + ws.on('play', function (id) { + if (!ws.controlled) return + let target = getClientByDevice(ws.controlled) + if (!target) return ws.emit('errno', { code: 100, error: 'Target device is not available.' }) + target.emit('play', id) + }) + + ws.on('seekto', function (num) { + if (!ws.controlled) return + let target = getClientByDevice(ws.controlled) + if (!target) return ws.emit('errno', { code: 100, error: 'Target device is not available.' }) + target.emit('seekto', num) + }) + + ws.on('volumeto', function (num) { + if (!ws.controlled) return + let target = getClientByDevice(ws.controlled) + if (!target) return ws.emit('errno', { code: 100, error: 'Target device is not available.' }) + target.emit('volumeto', num) + }) +}) + +module.exports = function (server) { + server.on('upgrade', function (request, socket, head) { + const pathname = url.parse(request.url).pathname + + if (pathname === '/remote') { + wss.handleUpgrade(request, socket, head, function (ws) { + wss.emit('connection', ws, request) + }) + } else { + socket.destroy() + } + }) +} diff --git a/src/server.js b/src/server.js index a8d2ef6..2c61bd5 100644 --- a/src/server.js +++ b/src/server.js @@ -2,10 +2,12 @@ import path from 'path' import sqlite from 'sqlite' import Promise from 'bluebird' import express from 'express' +import http from 'http' import https from 'https' import ffmpeg from 'fluent-ffmpeg' import lastfm from './lastfm' +import remote from './remote-control' require('express-async-errors') @@ -19,6 +21,14 @@ const dbPromise = Promise.resolve() 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')) +} + const router = express.Router() const sortfields = ['id', 'track', 'artist', 'title', 'album', 'year', 'file'] @@ -123,6 +133,9 @@ router.get('/tracks/search', async (req, res) => { let 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) {} } @@ -205,6 +218,9 @@ app.use('/', express.static(path.join(process.cwd(), 'public'))) const host = process.env.NODE_ENV === 'development' ? '0.0.0.0' : '127.0.0.1' -app.listen(port, host, function () { +// Initialize remote control +remote(server) + +server.listen(port, host, function () { console.log(`app running on port ${port}`) })