import fs from 'fs' import { sep } from 'path' import { v4 as uuidv4 } from 'uuid' // @ts-expect-error import deemix from 'deemix' import got from 'got' import { Settings, Listener } from './types' import { NotLoggedIn } from './helpers/errors' import { GUI_PACKAGE } from './helpers/paths' import { logger } from './helpers/logger' // Types const Downloader = deemix.downloader.Downloader const { Single, Collection, Convertable } = deemix.types.downloadObjects // Functions export const getAccessToken = deemix.utils.deezer.getAccessToken export const getArlFromAccessToken = deemix.utils.deezer.getArlFromAccessToken // Constants export const configFolder: string = deemix.utils.localpaths.getConfigFolder() export const defaultSettings: Settings = deemix.settings.DEFAULTS export const deemixVersion = require('../node_modules/deemix/package.json').version const currentVersionTemp = JSON.parse(String(fs.readFileSync(GUI_PACKAGE))).version export const currentVersion = currentVersionTemp === '0.0.0' ? 'continuous' : currentVersionTemp export const sessionDZ: any = {} export class DeemixApp { queueOrder: string[] queue: any currentJob: any deezerAvailable: string | null latestVersion: string | null plugins: any settings: any listener: Listener constructor(listener: Listener) { this.settings = deemix.settings.load(configFolder) this.queueOrder = [] this.queue = {} this.currentJob = null this.plugins = { // eslint-disable-next-line new-cap spotify: new deemix.plugins.spotify() } this.deezerAvailable = null this.latestVersion = null this.listener = listener this.plugins.spotify.setup() this.restoreQueueFromDisk() } async isDeezerAvailable(): Promise { if (this.deezerAvailable === null) { let response try { response = await got.get('https://www.deezer.com/', { headers: { Cookie: 'dz_lang=en; Domain=deezer.com; Path=/; Secure; hostOnly=false;' }, https: { rejectUnauthorized: false }, retry: 5 }) } catch (e) { logger.error(e) this.deezerAvailable = 'no-network' return this.deezerAvailable } const title = (response.body.match(/]*>([^<]+)<\/title>/)![1] || '').trim() this.deezerAvailable = title !== 'Deezer will soon be available in your country.' ? 'yes' : 'no' } return this.deezerAvailable } async getLatestVersion(force = false): Promise { if ((this.latestVersion === null || force) && !this.settings.disableUpdateCheck) { let response try { response = await got.get('https://deemix.app/gui/latest', { https: { rejectUnauthorized: false } }) } catch (e) { logger.error(e) this.latestVersion = 'NotFound' return this.latestVersion } this.latestVersion = response.body.trim() } return this.latestVersion } parseVersion(version: string | null): any { if (version === null || version === 'continuous' || version === 'NotFound') return null try { const matchResult = version.match(/(\d+)\.(\d+)\.(\d+)-r(\d+)\.(.+)/) || [] return { year: parseInt(matchResult[1]), month: parseInt(matchResult[2]), day: parseInt(matchResult[3]), revision: parseInt(matchResult[4]), commit: matchResult[5] || '' } } catch (e) { logger.error(e) return null } } isUpdateAvailable(): boolean { const currentVersionObj: any = this.parseVersion(currentVersion) const latestVersionObj: any = this.parseVersion(this.latestVersion) if (currentVersionObj === null || latestVersionObj === null) return false if (latestVersionObj.year > currentVersionObj.year) return true if (latestVersionObj.month > currentVersionObj.month) return true if (latestVersionObj.day > currentVersionObj.day) return true if (latestVersionObj.revision > currentVersionObj.revision) return true if ( latestVersionObj.revision === currentVersionObj.revision && latestVersionObj.commit !== currentVersionObj.commit ) return true return false } getSettings(): any { return { settings: this.settings, defaultSettings, spotifySettings: this.plugins.spotify.getSettings() } } saveSettings(newSettings: any, newSpotifySettings: any) { newSettings.executeCommand = this.settings.executeCommand deemix.settings.save(newSettings, configFolder) this.settings = newSettings this.plugins.spotify.saveSettings(newSpotifySettings) } getQueue() { const result: any = { queue: this.queue, queueOrder: this.queueOrder } if (this.currentJob && this.currentJob !== true) { result.current = this.currentJob.downloadObject.getSlimmedDict() } return result } async addToQueue(dz: any, url: string[], bitrate: number) { if (!dz.logged_in) throw new NotLoggedIn() let downloadObjs: any[] = [] const downloadErrors: any[] = [] let link: string = '' const requestUUID = uuidv4() if (url.length > 1) { this.listener.send('startGeneratingItems', { uuid: requestUUID, total: url.length }) } for (let i = 0; i < url.length; i++) { link = url[i] logger.info(`Adding ${link} to queue`) let downloadObj try { downloadObj = await deemix.generateDownloadObject(dz, link, bitrate, this.plugins, this.listener) } catch (e) { downloadErrors.push(e) } if (Array.isArray(downloadObj)) { downloadObjs = downloadObjs.concat(downloadObj) } else if (downloadObj) downloadObjs.push(downloadObj) } if (downloadErrors.length) { downloadErrors.forEach((e: any) => { if (!e.errid) logger.error(e) this.listener.send('queueError', { link: e.link, error: e.message, errid: e.errid }) }) } if (url.length > 1) { this.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(this.queue).includes(downloadObj.uuid)) { this.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') this.queueOrder.push(downloadObj.uuid) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(this.queueOrder)) this.queue[downloadObj.uuid] = downloadObj.getEssentialDict() this.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) this.listener.send('addedToQueue', downloadObjs[0].getSlimmedDict()) else this.listener.send('addedToQueue', slimmedObjects) this.startQueue(dz) return slimmedObjects } async startQueue(dz: any): Promise { do { if (this.currentJob !== null || this.queueOrder.length === 0) { // Should not start another download return null } this.currentJob = true // lock currentJob let currentUUID: string do { currentUUID = this.queueOrder.shift() || '' } while (this.queue[currentUUID] === undefined && this.queueOrder.length) this.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 this.plugins[downloadObject.plugin].convert( dz, downloadObject, this.settings, this.listener ) fs.writeFileSync( configFolder + `queue${sep}${downloadObject.uuid}.json`, JSON.stringify({ ...downloadObject.toDict(), status: 'inQueue' }) ) break } this.currentJob = new Downloader(dz, downloadObject, this.settings, this.listener) this.listener.send('startDownload', currentUUID) await this.currentJob.start() if (!downloadObject.isCanceled) { // Set status if (downloadObject.failed === downloadObject.size && downloadObject.size !== 0) { this.queue[currentUUID].status = 'failed' } else if (downloadObject.failed > 0) { this.queue[currentUUID].status = 'withErrors' } else { this.queue[currentUUID].status = 'completed' } const savedObject = downloadObject.getSlimmedDict() savedObject.status = this.queue[currentUUID].status // Save queue status this.queue[currentUUID] = savedObject fs.writeFileSync(configFolder + `queue${sep}${currentUUID}.json`, JSON.stringify(savedObject)) } logger.info(this.queueOrder) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(this.queueOrder)) this.currentJob = null } while (this.queueOrder.length) } cancelDownload(uuid: string) { if (Object.keys(this.queue).includes(uuid)) { switch (this.queue[uuid].status) { case 'downloading': this.currentJob.downloadObject.isCanceled = true this.listener.send('cancellingCurrentItem', uuid) break case 'inQueue': this.queueOrder.splice(this.queueOrder.indexOf(uuid), 1) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(this.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 this.listener.send('removedFromQueue', uuid) break } fs.unlinkSync(configFolder + `queue${sep}${uuid}.json`) delete this.queue[uuid] } } cancelAllDownloads() { this.queueOrder = [] let currentItem: string | null = null Object.values(this.queue).forEach((downloadObject: any) => { if (downloadObject.status === 'downloading') { this.currentJob.downloadObject.isCanceled = true this.listener.send('cancellingCurrentItem', downloadObject.uuid) currentItem = downloadObject.uuid } fs.unlinkSync(configFolder + `queue${sep}${downloadObject.uuid}.json`) delete this.queue[downloadObject.uuid] }) fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(this.queueOrder)) this.listener.send('removedAllDownloads', currentItem) } clearCompletedDownloads() { Object.values(this.queue).forEach((downloadObject: any) => { if (downloadObject.status === 'completed') { fs.unlinkSync(configFolder + `queue${sep}${downloadObject.uuid}.json`) delete this.queue[downloadObject.uuid] } }) this.listener.send('removedFinishedDownloads') } 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') { try { this.queueOrder = JSON.parse(fs.readFileSync(configFolder + `queue${sep}order.json`).toString()) } catch { this.queueOrder = [] fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(this.queueOrder)) } } else { let currentItem: any try { currentItem = JSON.parse(fs.readFileSync(configFolder + `queue${sep}${filename}`).toString()) } catch { fs.unlinkSync(configFolder + `queue${sep}${filename}`) return } 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 } this.queue[downloadObject.uuid] = downloadObject.getEssentialDict() this.queue[downloadObject.uuid].status = 'inQueue' } else { this.queue[currentItem.uuid] = currentItem } } }) } }