import path from 'path' import sanitize from 'sanitize-filename' import * as asn from './common/async' import * as dl from './common/download' import { dbPromise } from './database' import { URL } from 'url' const fs = require('fs').promises const values = require(path.join(process.cwd(), 'values.json')) const memexpire = 1800 const externalTracks = {} const downloadQueue = [] let downloading = false async function downloadLocally (id) { const info = await getTrackMetaReal(id) if (!info) throw new Error('No track with this ID in external list.') const file = await dl.fetchVideo(info) let filename = info.artist + ' - ' + info.title + '.mp3' filename = sanitize(filename) 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) const db = await dbPromise const 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 const trdata = externalTracks[id] // Check for expiry if (trdata.file && trdata.expires > Date.now()) { return Object.assign({}, trdata) } const trsrch = 'ytsearch3:' + trdata.artist + ' - ' + trdata.title const dldata = await dl.getVideoInfo(trsrch) let bestMatch if (dldata.length === 1) bestMatch = dldata[0] let candidates = [] for (const i in dldata) { const obj = dldata[i] const 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]) } // 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) } function addListExternal (id, obj) { if (externalTracks[id]) return Object.assign({}, externalTracks[id]) externalTracks[id] = obj return obj } async function searchURL (url) { const urlp = new URL(url) if (urlp.hostname.indexOf('youtube.com') !== -1 || urlp.hostname.indexOf('youtu.be') !== -1) { const id = urlp.searchParams.get('v') || urlp.pathname.substr(1) const ytb = await dl.getVideoInfo(id) externalTracks[id] = { id: id, title: ytb.title, artist: ytb.artist || ytb.uploader, file: ytb.url, duration: ytb.duration, expires: Date.now() + memexpire * 1000, external: true } return [externalTracks[id]] } return [] } export { downloadLocally, invokeDownload, addListExternal, getTrackMetaReal, searchURL }