import path from 'path' import asn from './common/async' import dl from './common/download' import crypto from 'crypto' const fs = require('fs').promises const values = require(path.join(process.cwd(), 'values.json')) const memexpire = 1800 let externalTracks = {} let downloadQueue = [] let downloading = false let dbPromise function createHash (data) { return crypto .createHash('sha1') .update(data.artist + data.name) .digest('hex') .substr(0, 8) } 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.') } } async function getTrackMetaReal (id) { if (!id || !externalTracks[id]) return null let trdata = externalTracks[id] // Check for expiry if (trdata.file && trdata.expires > Date.now()) { return Object.assign({}, trdata) } let trsrch = 'ytsearch3:' + trdata.artist + ' - ' + trdata.title let dldata = await dl.getVideoInfo(trsrch) 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) } if (candidates.length && !bestMatch) { // 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 if (!candidates.length && !bestMatch) bestMatch = dldata[0] externalTracks[id] = { id: trdata.id, title: trdata.title, artist: trdata.artist, file: bestMatch.url, duration: bestMatch.duration, expires: Date.now() + memexpire * 1000, external: true } return Object.assign({}, externalTracks[id]) } async function search (track, limit = 30) { if (!values.lastfm) return [] let data try { data = await asn.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) { return [] } let final = [] for (let i in data.results.trackmatches.track) { let res = data.results.trackmatches.track[i] let clean = { id: createHash(res), artist: res.artist, title: res.name, external: true, mbid: res.mbid } 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 } // 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) } export default function (db) { dbPromise = db return { search, getTrackMetaReal, invokeDownload } }