2019-01-25 15:00:35 +00:00
|
|
|
import path from 'path'
|
2019-12-18 12:57:02 +00:00
|
|
|
import rp from 'request-promise-native'
|
2019-01-26 22:35:09 +00:00
|
|
|
import asn from './common/async'
|
2019-01-25 15:00:35 +00:00
|
|
|
import dl from './common/download'
|
|
|
|
import crypto from 'crypto'
|
2019-12-17 17:52:18 +00:00
|
|
|
import { dbPromise } from './database'
|
2019-12-18 12:57:02 +00:00
|
|
|
import { parseStringPromise } from 'xml2js'
|
2019-01-25 15:00:35 +00:00
|
|
|
|
2019-01-26 22:35:09 +00:00
|
|
|
const fs = require('fs').promises
|
2019-01-25 15:00:35 +00:00
|
|
|
const values = require(path.join(process.cwd(), 'values.json'))
|
2019-02-07 18:04:15 +00:00
|
|
|
const memexpire = 1800
|
2019-01-25 15:00:35 +00:00
|
|
|
|
|
|
|
let externalTracks = {}
|
2019-01-26 22:35:09 +00:00
|
|
|
let downloadQueue = []
|
|
|
|
let downloading = false
|
2019-01-25 15:00:35 +00:00
|
|
|
|
|
|
|
function createHash (data) {
|
2019-01-25 21:11:58 +00:00
|
|
|
return crypto
|
|
|
|
.createHash('sha1')
|
|
|
|
.update(data.artist + data.name)
|
|
|
|
.digest('hex')
|
2019-01-25 15:00:35 +00:00
|
|
|
.substr(0, 8)
|
|
|
|
}
|
|
|
|
|
2019-01-26 22:35:09 +00:00
|
|
|
async function downloadLocally (id) {
|
|
|
|
let info = await getTrackMetaReal(id)
|
|
|
|
if (!info) throw new Error('No track with this ID in external list.')
|
|
|
|
let file = await dl.fetchVideo(info)
|
|
|
|
let filename = info.artist + ' - ' + info.title + '.mp3'
|
|
|
|
info.file = path.join(values.directory, filename)
|
|
|
|
await asn.promiseExec(`ffmpeg -i "${file.source}" -metadata artist="${info.artist}" -metadata title="${info.title}" -codec copy "${info.file}"`)
|
|
|
|
await fs.unlink(file.source)
|
|
|
|
let db = await dbPromise
|
|
|
|
let ins = await asn.insertDB(db, info)
|
|
|
|
if (!ins) {
|
|
|
|
throw new Error('A track of this description already exists in the database.')
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
async function getTrackMetaReal (id) {
|
|
|
|
if (!id || !externalTracks[id]) return null
|
|
|
|
let trdata = externalTracks[id]
|
2019-02-07 18:04:15 +00:00
|
|
|
|
|
|
|
// Check for expiry
|
|
|
|
if (trdata.file && trdata.expires > Date.now()) {
|
|
|
|
return Object.assign({}, trdata)
|
|
|
|
}
|
2019-01-25 15:00:35 +00:00
|
|
|
|
2019-11-04 20:51:10 +00:00
|
|
|
let trsrch = 'ytsearch3:' + trdata.artist + ' - ' + trdata.title
|
2019-01-25 15:00:35 +00:00
|
|
|
let dldata = await dl.getVideoInfo(trsrch)
|
|
|
|
|
2019-11-04 20:51:10 +00:00
|
|
|
let bestMatch
|
|
|
|
if (dldata.length === 1) bestMatch = dldata[0]
|
|
|
|
|
|
|
|
let candidates = []
|
|
|
|
for (let i in dldata) {
|
|
|
|
let obj = dldata[i]
|
|
|
|
let title = obj.title.toLowerCase()
|
|
|
|
|
|
|
|
// Skip any video with 'video' in it, but keep lyric videos
|
|
|
|
if (title.indexOf('video') !== -1 && title.indexOf('lyric') === -1) continue
|
|
|
|
|
|
|
|
// If the title has 'audio' in it, it might be the best match
|
|
|
|
if (title.indexOf('audio') !== -1) {
|
|
|
|
bestMatch = obj
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
|
|
|
candidates.push(obj)
|
|
|
|
}
|
|
|
|
|
2019-11-14 17:08:48 +00:00
|
|
|
if (candidates.length && !bestMatch) {
|
2019-11-04 20:51:10 +00:00
|
|
|
// Sort candidates by view count
|
|
|
|
candidates = candidates.sort(function (a, b) {
|
|
|
|
return b.view_count - a.view_count
|
|
|
|
})
|
|
|
|
|
|
|
|
// Select the one with the most views
|
|
|
|
bestMatch = candidates[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there were no suitable candidates, just take the first response
|
2019-11-14 17:08:48 +00:00
|
|
|
if (!candidates.length && !bestMatch) bestMatch = dldata[0]
|
2019-11-04 20:51:10 +00:00
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
externalTracks[id] = {
|
|
|
|
id: trdata.id,
|
|
|
|
title: trdata.title,
|
|
|
|
artist: trdata.artist,
|
2019-11-04 20:51:10 +00:00
|
|
|
file: bestMatch.url,
|
|
|
|
duration: bestMatch.duration,
|
2019-02-07 18:04:15 +00:00
|
|
|
expires: Date.now() + memexpire * 1000,
|
2019-01-25 15:00:35 +00:00
|
|
|
external: true
|
|
|
|
}
|
|
|
|
|
2019-01-25 21:11:58 +00:00
|
|
|
return Object.assign({}, externalTracks[id])
|
2019-01-25 15:00:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function search (track, limit = 30) {
|
|
|
|
if (!values.lastfm) return []
|
2019-01-25 21:11:58 +00:00
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
let data
|
|
|
|
try {
|
2019-12-18 12:57:02 +00:00
|
|
|
data = await rp(`http://ws.audioscrobbler.com/2.0/?method=track.search&track=${track}&api_key=${values.lastfm.key}&format=json&limit=${limit}`)
|
2019-01-25 15:00:35 +00:00
|
|
|
data = JSON.parse(data)
|
2019-01-25 21:11:58 +00:00
|
|
|
|
|
|
|
if (!data.results || !data.results.trackmatches || !data.results.trackmatches.track) {
|
|
|
|
throw new Error('No results')
|
|
|
|
}
|
2019-01-25 15:00:35 +00:00
|
|
|
} catch (e) {
|
|
|
|
return []
|
|
|
|
}
|
2019-01-25 21:11:58 +00:00
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
let final = []
|
|
|
|
for (let i in data.results.trackmatches.track) {
|
|
|
|
let res = data.results.trackmatches.track[i]
|
2019-01-25 21:11:58 +00:00
|
|
|
let clean = {
|
|
|
|
id: createHash(res),
|
|
|
|
artist: res.artist,
|
|
|
|
title: res.name,
|
2019-02-07 18:04:15 +00:00
|
|
|
external: true,
|
|
|
|
mbid: res.mbid
|
2019-01-25 15:00:35 +00:00
|
|
|
}
|
2019-01-25 21:11:58 +00:00
|
|
|
|
|
|
|
if (externalTracks[clean.id]) {
|
|
|
|
// Copy object
|
|
|
|
clean = Object.assign({}, externalTracks[clean.id])
|
|
|
|
} else {
|
|
|
|
// Save in cache
|
|
|
|
externalTracks[clean.id] = clean
|
|
|
|
}
|
|
|
|
|
|
|
|
final.push(clean)
|
2019-01-25 15:00:35 +00:00
|
|
|
}
|
2019-01-25 21:11:58 +00:00
|
|
|
|
2019-01-25 15:00:35 +00:00
|
|
|
return final
|
|
|
|
}
|
|
|
|
|
2019-01-26 22:35:09 +00:00
|
|
|
// Download thread
|
|
|
|
let dltd = null
|
|
|
|
function invokeDownload (add) {
|
|
|
|
if (add) downloadQueue.push(add)
|
|
|
|
if (dltd) return
|
|
|
|
dltd = setTimeout(function (argument) {
|
|
|
|
dltd = null
|
|
|
|
if (downloading) return invokeDownload()
|
|
|
|
if (!downloadQueue.length) return
|
|
|
|
downloading = true
|
|
|
|
downloadLocally(downloadQueue.shift()).then(() => {
|
|
|
|
downloading = false
|
|
|
|
if (downloadQueue.length) invokeDownload()
|
|
|
|
}).catch((e) => {
|
|
|
|
console.error(e)
|
|
|
|
downloading = false
|
|
|
|
if (downloadQueue.length) invokeDownload()
|
|
|
|
})
|
|
|
|
}, 2 * 1000)
|
|
|
|
}
|
|
|
|
|
2019-12-18 12:57:02 +00:00
|
|
|
// Authentication
|
|
|
|
|
|
|
|
function getAPISig (params) {
|
|
|
|
let allStrings = []
|
|
|
|
let qs = {}
|
|
|
|
params['api_key'] = values.lastfm.key
|
|
|
|
for (let key in params) {
|
|
|
|
let val = params[key]
|
|
|
|
if (val == null || val === '') continue
|
|
|
|
allStrings.push(key + val)
|
|
|
|
qs[key] = val
|
|
|
|
}
|
|
|
|
allStrings = allStrings.sort()
|
|
|
|
allStrings.push(values.lastfm.secret)
|
|
|
|
qs['api_sig'] = crypto.createHash('md5').update(allStrings.join('')).digest('hex')
|
|
|
|
return qs
|
|
|
|
}
|
|
|
|
|
|
|
|
function getAuthURL () {
|
|
|
|
return 'http://www.last.fm/api/auth/?api_key=' + values.lastfm.key + '&cb=' + values.lastfm.redirectUri
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSession (token) {
|
|
|
|
let sessSig = getAPISig({ token, method: 'auth.getSession' })
|
|
|
|
let res = await rp('http://ws.audioscrobbler.com/2.0/', { qs: sessSig })
|
|
|
|
let rep = await parseStringPromise(res)
|
|
|
|
let name = rep.lfm.session[0].name[0]
|
|
|
|
let key = rep.lfm.session[0].key[0]
|
|
|
|
return { name, key }
|
|
|
|
}
|
|
|
|
|
|
|
|
async function storeSession (userId, session) {
|
|
|
|
if (!session.name || !session.key) throw new Error('Invalid session parameter.')
|
|
|
|
let db = await dbPromise
|
|
|
|
let existing = await db.get('SELECT * FROM LastFM WHERE userId = ?', userId)
|
|
|
|
if (existing) {
|
|
|
|
await db.run('UPDATE LastFM SET name = ?, key = ? WHERE userId = ?', session.name, session.key, userId)
|
|
|
|
} else {
|
|
|
|
await db.run('INSERT INTO LastFM (userId,name,key,created) VALUES (?,?,?,?)', userId, session.name, session.key, new Date())
|
|
|
|
}
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
async function disregardSession (userId) {
|
|
|
|
let db = await dbPromise
|
|
|
|
return db.run('DELETE FROM LastFM WHERE userId = ?', userId)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function getSessionForUser (userId) {
|
|
|
|
let db = await dbPromise
|
|
|
|
return db.get('SELECT * FROM LastFM WHERE userId = ?', userId)
|
|
|
|
}
|
|
|
|
|
|
|
|
async function scrobbleTrack (userId, trackData) {
|
|
|
|
let sess = await getSessionForUser(userId)
|
|
|
|
if (!sess) throw new Error('User does not have a LastFM session.')
|
|
|
|
let scrobbleSig = getAPISig({
|
|
|
|
sk: sess.key,
|
|
|
|
method: 'track.scrobble',
|
|
|
|
artist: trackData.artist || 'Unknown',
|
|
|
|
title: trackData.title,
|
|
|
|
album: trackData.album,
|
|
|
|
duration: trackData.duration,
|
|
|
|
mbid: trackData.mbid,
|
|
|
|
timestamp: Math.floor(Date.now() / 1000)
|
|
|
|
})
|
|
|
|
// let res =
|
|
|
|
await rp.post('http://ws.audioscrobbler.com/2.0/', { form: scrobbleSig })
|
|
|
|
// let rep = await parseStringPromise(res)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
export default { search, getTrackMetaReal, invokeDownload, getAPISig, getAuthURL, getSession,
|
|
|
|
getSessionForUser, storeSession, disregardSession, scrobbleTrack }
|