import fs from 'fs' import { sep } from 'path' import { v4 as uuidv4 } from 'uuid' // @ts-expect-error import deemix from 'deemix' import WebSocket from 'ws' import { wss } from './app' import { Settings } from './types' import { NotLoggedIn } from './helpers/errors' const Downloader = deemix.downloader.Downloader const { Single, Collection, Convertable } = deemix.types.downloadObjects export const defaultSettings: Settings = deemix.settings.DEFAULTS export const configFolder: string = deemix.utils.localpaths.getConfigFolder() export const sessionDZ: any = {} let settings: any = deemix.settings.load(configFolder) export const getAccessToken = deemix.utils.deezer.getAccessToken export const getArlFromAccessToken = deemix.utils.deezer.getArlFromAccessToken export const plugins: any = { // eslint-disable-next-line new-cap spotify: new deemix.plugins.spotify() } plugins.spotify.setup() export const listener = { send(key: string, data?: any) { if (data) console.log(key, data) else console.log(key) if (['downloadInfo', 'downloadWarn'].includes(key)) return wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify({ key, data })) } }) } } export function getSettings(): any { return { settings, defaultSettings, spotifySettings: plugins.spotify.getCredentials() } } export function saveSettings(newSettings: any, newSpotifySettings: any) { deemix.settings.save(newSettings, configFolder) settings = newSettings plugins.spotify.setCredentials(newSpotifySettings) } let queueOrder: string[] = [] const queue: any = {} let currentJob: any = null restoreQueueFromDisk() export function getQueue() { const result: any = { queue, queueOrder } if (currentJob && currentJob !== true) { result.current = currentJob.downloadObject.getSlimmedDict() } return result } export async function addToQueue(dz: any, url: string[], bitrate: number) { if (!dz.logged_in) throw new NotLoggedIn() let downloadObjs: any[] = [] let link: string = '' const requestUUID = uuidv4() if (url.length > 1) { listener.send('startGeneratingItems', { uuid: requestUUID, total: url.length }) } for (let i = 0; i < url.length; i++) { link = url[i] console.log(`Adding ${link} to queue`) const downloadObj = await deemix.generateDownloadObject(dz, link, bitrate, plugins, listener) if (Array.isArray(downloadObj)) { downloadObjs = downloadObjs.concat(downloadObj) } else { downloadObjs.push(downloadObj) } } if (url.length > 1) { listener.send('finishGeneratingItems', { uuid: requestUUID, total: downloadObjs.length }) } const slimmedObjects: any[] = [] downloadObjs.forEach((downloadObj: any, pos: number) => { // Check if element is already in queue if (Object.keys(queue).includes(downloadObj.uuid)) { listener.send('alreadyInQueue', downloadObj.getEssentialDict()) delete downloadObjs[pos] return } // Save queue status when adding something to the queue if (!fs.existsSync(configFolder + 'queue')) fs.mkdirSync(configFolder + 'queue') queueOrder.push(downloadObj.uuid) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(queueOrder)) queue[downloadObj.uuid] = downloadObj.getEssentialDict() queue[downloadObj.uuid].status = 'inQueue' const savedObject = downloadObj.toDict() savedObject.status = 'inQueue' fs.writeFileSync(configFolder + `queue${sep}${downloadObj.uuid}.json`, JSON.stringify(savedObject)) slimmedObjects.push(downloadObj.getSlimmedDict()) }) const isSingleObject = downloadObjs.length === 1 if (isSingleObject) listener.send('addedToQueue', downloadObjs[0].getSlimmedDict()) else listener.send('addedToQueue', slimmedObjects) startQueue(dz) return slimmedObjects } export async function startQueue(dz: any): Promise { do { if (currentJob !== null || queueOrder.length === 0) { // Should not start another download return null } currentJob = true // lock currentJob const currentUUID: string = queueOrder.shift() || '' console.log(currentUUID) queue[currentUUID].status = 'downloading' const currentItem: any = JSON.parse(fs.readFileSync(configFolder + `queue${sep}${currentUUID}.json`).toString()) let downloadObject: any switch (currentItem.__type__) { case 'Single': downloadObject = new Single(currentItem) break case 'Collection': downloadObject = new Collection(currentItem) break case 'Convertable': downloadObject = new Convertable(currentItem) downloadObject = await plugins[downloadObject.plugin].convert(dz, downloadObject, settings, listener) fs.writeFileSync( configFolder + `queue${sep}${downloadObject.uuid}.json`, JSON.stringify({ ...downloadObject.toDict(), status: 'inQueue' }) ) break } currentJob = new Downloader(dz, downloadObject, settings, listener) listener.send('startDownload', currentUUID) await currentJob.start() if (!downloadObject.isCanceled) { // Set status if (downloadObject.failed === downloadObject.size) { queue[currentUUID].status = 'failed' } else if (downloadObject.failed > 0) { queue[currentUUID].status = 'withErrors' } else { queue[currentUUID].status = 'completed' } const savedObject = downloadObject.getSlimmedDict() savedObject.status = queue[currentUUID].status // Save queue status queue[currentUUID] = savedObject fs.writeFileSync(configFolder + `queue${sep}${currentUUID}.json`, JSON.stringify(savedObject)) } console.log(queueOrder) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(queueOrder)) currentJob = null } while (queueOrder.length) } export function cancelDownload(uuid: string) { if (Object.keys(queue).includes(uuid)) { switch (queue[uuid].status) { case 'downloading': currentJob.downloadObject.isCanceled = true listener.send('cancellingCurrentItem', uuid) break case 'inQueue': queueOrder.splice(queueOrder.indexOf(uuid), 1) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(queueOrder)) // break // eslint-disable-next-line no-fallthrough default: // This gets called even in the 'inQueue' case. Is this the expected behaviour? If no, de-comment the break listener.send('removedFromQueue', uuid) break } fs.unlinkSync(configFolder + `queue${sep}${uuid}.json`) delete queue[uuid] } } export function cancelAllDownloads() { queueOrder = [] let currentItem: string | null = null Object.values(queue).forEach((downloadObject: any) => { if (downloadObject.status === 'downloading') { currentJob.downloadObject.isCanceled = true listener.send('cancellingCurrentItem', downloadObject.uuid) currentItem = downloadObject.uuid } fs.unlinkSync(configFolder + `queue${sep}${downloadObject.uuid}.json`) delete queue[downloadObject.uuid] }) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(queueOrder)) listener.send('removedAllDownloads', currentItem) } export function clearCompletedDownloads() { Object.values(queue).forEach((downloadObject: any) => { if (downloadObject.status === 'completed') { fs.unlinkSync(configFolder + `queue${sep}${downloadObject.uuid}.json`) delete queue[downloadObject.uuid] } }) listener.send('removedFinishedDownloads') } export function restoreQueueFromDisk() { if (!fs.existsSync(configFolder + 'queue')) fs.mkdirSync(configFolder + 'queue') const allItems: string[] = fs.readdirSync(configFolder + 'queue') allItems.forEach((filename: string) => { if (filename === 'order.json') { queueOrder = JSON.parse(fs.readFileSync(configFolder + `queue${sep}order.json`).toString()) } else { const currentItem: any = JSON.parse(fs.readFileSync(configFolder + `queue${sep}${filename}`).toString()) if (currentItem.status === 'inQueue') { let downloadObject: any switch (currentItem.__type__) { case 'Single': downloadObject = new Single(currentItem) break case 'Collection': downloadObject = new Collection(currentItem) break case 'Convertable': downloadObject = new Convertable(currentItem) break } queue[downloadObject.uuid] = downloadObject.getEssentialDict() queue[downloadObject.uuid].status = 'inQueue' } else { queue[currentItem.uuid] = currentItem } } }) }