diff --git a/package.json b/package.json
index 1d1f57c..0b7f9f6 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"bluebird": "^3.5.2",
"express": "^4.16.3",
"express-async-errors": "^3.0.0",
+ "fluent-ffmpeg": "^2.1.2",
"fs-extra": "^7.0.0",
"socket.io": "^2.1.1",
"sqlite": "^3.0.0",
diff --git a/public/index.css b/public/index.css
index 18b4bde..ba293e8 100644
--- a/public/index.css
+++ b/public/index.css
@@ -82,6 +82,9 @@ td,th {
tr:nth-child(even) {
background-color: #0f1b23;
}
+tr.external td {
+ background-color: rgba(0, 255, 233, 0.25);
+}
.pages {
display: flex;
flex-direction: row;
diff --git a/public/index.html b/public/index.html
index 5a053b3..a740724 100644
--- a/public/index.html
+++ b/public/index.html
@@ -83,6 +83,10 @@
+
+
+
+
diff --git a/public/index.js b/public/index.js
index 3685078..241602c 100644
--- a/public/index.js
+++ b/public/index.js
@@ -18,6 +18,7 @@
Duration | \
'
var nowPlaying = 0
+ var externalStream = false
var pageNum = 0
var pages = 1
@@ -41,6 +42,7 @@
var options = {
autoplay: true,
trackids: true,
+ streamable: true,
sortby: 'id',
sortdir: 'asc',
}
@@ -257,12 +259,13 @@
let trTag = document.createElement('tr')
trTag.className = 'track'
trTag.setAttribute('data-id', track.id)
- trTag.innerHTML += '' + (options.trackids ? track.id : (track.track || ' ')) + ' | '
+ if (track.external) trTag.className += ' external'
+ trTag.innerHTML += '' + (track.external ? 'Stream' : (options.trackids ? track.id : (track.track || ' '))) + ' | '
trTag.innerHTML += '' + title + ' | '
trTag.innerHTML += '' + (track.artist || '') + ' | '
trTag.innerHTML += '' + (track.album || '') + ' | '
trTag.innerHTML += '' + (track.year || '') + ' | '
- trTag.innerHTML += '' + toHHMMSS(track.duration) + ' | '
+ trTag.innerHTML += '' + (track.duration ? toHHMMSS(track.duration) : '') + ' | '
return trTag
}
@@ -383,7 +386,8 @@
clear.style.display = 'block'
}
- httpGet('/api/tracks/search?q=' + query + '&page=' + page + '&sort=' + options.sortby + '&sortdir=' + options.sortdir).then(function (data) {
+ httpGet('/api/tracks/search?q=' + query + '&page=' + page + '&sort=' + options.sortby + '&sortdir=' +
+ options.sortdir + '&streamable=' + (+options.streamable)).then(function (data) {
pageNum = page
pages = data.pageCount
constructList(data.tracks)
@@ -400,7 +404,8 @@
window.resumeAudioContexts()
- audio.src = '/api/serve/by-id/' + id
+ externalStream = (data.external === true)
+ audio.src = data.play_url || ('/api/serve/by-id/' + id)
audio.play()
updateQT(q)
@@ -412,8 +417,9 @@
}
function playNext () {
- if (queue.length === 0) return play(nowPlaying + 1)
- play(queue.shift(), true)
+ if (queue.length !== 0) return play(queue.shift(), true)
+ if (externalStream) return
+ play(nowPlaying + 1)
}
function showPlaylist (pid) {
@@ -510,9 +516,9 @@
}, false)
audio.addEventListener('ended', function (e) {
- if (options.autoplay) {
- playNext(e)
- }
+ if (externalStream) return
+ if (!options.autoplay) return
+ playNext(e)
}, false)
clear.addEventListener('click', function () {
diff --git a/public/player.js b/public/player.js
index 9382e31..ede6f19 100644
--- a/public/player.js
+++ b/public/player.js
@@ -61,13 +61,16 @@
}
function updateSeeker () {
- if (isNaN(audio.duration) || audio.duration === 0) return
+ if (audio.duration === Infinity || isNaN(audio.duration)) {
+ ts.innerHTML = toHHMMSS(audio.currentTime)
- ts.innerHTML = toHHMMSS(audio.currentTime) + ' / ' + toHHMMSS(audio.duration)
+ seekBar.style.width = '0%'
+ } else {
+ ts.innerHTML = toHHMMSS(audio.currentTime) + ' / ' + toHHMMSS(audio.duration)
+ let progress = 100 * audio.currentTime / audio.duration
- let progress = 100 * audio.currentTime / audio.duration
-
- seekBar.style.width = progress + '%'
+ seekBar.style.width = progress + '%'
+ }
}
function updateVolume () {
diff --git a/src/common/async.js b/src/common/async.js
index eea02e7..93fe5b0 100644
--- a/src/common/async.js
+++ b/src/common/async.js
@@ -1,6 +1,8 @@
import {exec} from 'child_process'
import path from 'path'
import fs from 'fs-extra'
+import url from 'url'
+import qs from 'querystring'
function filewalker (dir, done) {
let results = []
@@ -83,4 +85,54 @@ function copyAsync (fsrc, fdst) {
})
}
-export default {getFiles, promiseExec, askAsync, insertDB, copyAsync}
+function HTTP_GET (link, headers = {}) {
+ let parsed = url.parse(link)
+ let opts = {
+ host: parsed.hostname,
+ port: parsed.port,
+ path: parsed.path,
+ headers: {
+ 'Accept': '*/*',
+ 'Accept-Language': 'en-US'
+ }
+ }
+
+ if (headers) {
+ opts.headers = Object.assign(opts.headers, headers)
+ }
+
+ let reqTimeOut
+
+ let httpModule = parsed.protocol === 'https:' ? require('https') : require('http')
+ return new Promise((resolve, reject) => {
+ let req = httpModule.get(opts, (res) => {
+ if (res.statusCode === 302 || res.statusCode === 301) {
+ return reject(new Error('Invalid endpoint'))
+ }
+
+ let data = ''
+
+ reqTimeOut = setTimeout(() => {
+ req.abort()
+ data = null
+ reject(new Error('Request took too long!'))
+ }, 5000)
+
+ res.on('data', (chunk) => {
+ data += chunk
+ })
+
+ res.on('end', () => {
+ clearTimeout(reqTimeOut)
+
+ resolve(data)
+ })
+ }).on('error', (e) => {
+ reject(new Error(e.message))
+ })
+
+ req.setTimeout(10000)
+ })
+}
+
+export default {getFiles, promiseExec, askAsync, insertDB, copyAsync, GET: HTTP_GET}
diff --git a/src/common/download.js b/src/common/download.js
index b0d0ace..cefd1d7 100644
--- a/src/common/download.js
+++ b/src/common/download.js
@@ -1,4 +1,6 @@
import {spawn} from 'child_process'
+import ffmpeg from 'fluent-ffmpeg'
+
import path from 'path'
import asn from './async'
@@ -55,18 +57,12 @@ function getVideoInfo (arg) {
function fetchVideo (data) {
return new Promise((resolve, reject) => {
- if (data.acodec !== 'mp3' || data.vcodec !== 'none') {
- let tempName = path.join(process.cwd(), `/tmp.yt.${data.id}.mp3`)
- let ffmpeg = spawn('ffmpeg', ['-hide_banner', '-i', data.url, '-codec:a', 'libmp3lame', '-q:a', 2, '-joint_stereo', 1, '-y', tempName])
-
- ffmpeg.stdout.pipe(process.stderr)
- ffmpeg.stderr.pipe(process.stderr)
-
- ffmpeg.on('error', function (e) {
- reject(e)
- })
-
- ffmpeg.on('close', function () {
+ let tempName = path.join(process.cwd(), `/tmp.yt.${data.id}.mp3`)
+ ffmpeg(data.url)
+ .audioCodec('libmp3lame')
+ .format('mp3')
+ .on('error', reject)
+ .on('end', function () {
resolve({
title: data.title,
artist: data.uploader,
@@ -75,9 +71,8 @@ function fetchVideo (data) {
source: tempName
})
})
- } else {
- reject(new Error('Invalid format returned.'))
- }
+ .output(tempName)
+ .run()
})
}
diff --git a/src/lastfm.js b/src/lastfm.js
new file mode 100644
index 0000000..2d936e5
--- /dev/null
+++ b/src/lastfm.js
@@ -0,0 +1,60 @@
+import path from 'path'
+import as from './common/async'
+import dl from './common/download'
+import crypto from 'crypto'
+
+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')
+ .substr(0, 8)
+}
+
+async function getTrackMetaReal (id) {
+ if (!id || !externalTracks[id]) return null
+ let trdata = externalTracks[id]
+ if (trdata.file) return trdata
+
+ let trsrch = 'ytsearch1:' + trdata.artist + ' - ' + trdata.title
+ let dldata = await dl.getVideoInfo(trsrch)
+
+ externalTracks[id] = {
+ id: trdata.id,
+ title: trdata.title,
+ artist: trdata.artist,
+ file: dldata.url,
+ duration: dldata.duration,
+ external: true
+ }
+
+ return 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)
+ } 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
+ }
+ }
+ return final
+}
+
+export default {search, getTrackMetaReal}
diff --git a/src/server.js b/src/server.js
index 314083e..a8d2ef6 100644
--- a/src/server.js
+++ b/src/server.js
@@ -2,6 +2,10 @@ import path from 'path'
import sqlite from 'sqlite'
import Promise from 'bluebird'
import express from 'express'
+import https from 'https'
+import ffmpeg from 'fluent-ffmpeg'
+
+import lastfm from './lastfm'
require('express-async-errors')
@@ -57,6 +61,7 @@ router.get('/tracks', async (req, res) => {
router.get('/tracks/search', async (req, res) => {
let query = req.query.q
+ let streamable = (req.query.streamable === '1')
let qr = ''
let exact = false
@@ -82,6 +87,7 @@ router.get('/tracks/search', async (req, res) => {
exact = true
}
+ let original = String(query)
if (!exact) query = `%${query}%`
let sort = req.query.sort
@@ -110,6 +116,17 @@ router.get('/tracks/search', async (req, res) => {
let tracks = await db.all(`SELECT * FROM Track WHERE ${qr} LIKE ? ORDER BY ${sort} ${sortdir.toUpperCase()} LIMIT ? OFFSET ?`,
query, tracksPerPage, offset)
+ 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)
+ }
+ } catch (e) {}
+ }
+
for (let i in tracks) {
delete tracks[i].file
}
@@ -121,9 +138,13 @@ router.get('/tracks/search', async (req, res) => {
router.get('/track/:id', async (req, res, next) => {
let id = req.params.id
+
let db = await dbPromise
let track = await db.get('SELECT * FROM Track WHERE id = ?', id)
- if (!track) return next(new Error('404 file not found'))
+ if (!track) {
+ track = await lastfm.getTrackMetaReal(id)
+ if (!track) return next(new Error('404 file not found'))
+ }
delete track.file
@@ -152,16 +173,23 @@ router.get('/playlist/:id', async (req, res, next) => {
router.get('/serve/by-id/:id', async (req, res, next) => {
let id = req.params.id
+ let dl = (req.query.dl === '1')
let db = await dbPromise
let track = await db.get('SELECT file FROM Track WHERE id = ?', id)
- if (!track) return next(new Error('404 file not found'))
+ if (!track) {
+ track = await lastfm.getTrackMetaReal(id)
+ if (!track) return next(new Error('404 file not found'))
+ return ffmpeg(track.file)
+ .audioCodec('libmp3lame')
+ .format('mp3')
+ .on('error', (e) => console.error(e))
+ .pipe(res, {end: true})
+ }
let fpath = path.resolve(track.file)
res.set('Cache-Control', 'public, max-age=31557600')
- if (req.query.dl && parseInt(req.query.dl) === 1) {
- return res.download(fpath)
- }
+ if (dl) return res.download(fpath)
res.redirect('/file/track' + fpath.substring(values.directory.length))
})