Major code rework

This commit is contained in:
RemixDev 2022-02-05 01:15:26 +01:00
parent 5d29424c3a
commit 87164f0644
49 changed files with 562 additions and 927 deletions

View File

@ -10,13 +10,15 @@ const argv = yargs(hideBin(process.argv)).options({
host: { type: 'string', default: '127.0.0.1' }, host: { type: 'string', default: '127.0.0.1' },
dev: { type: 'boolean', default: false} dev: { type: 'boolean', default: false}
}).argv }).argv
const server = require('./server/dist/app.js') const { DeemixServer }= require('./server/dist/app.js')
const PORT = process.env.DEEMIX_SERVER_PORT || argv.port const PORT = process.env.DEEMIX_SERVER_PORT || argv.port
process.env.DEEMIX_SERVER_PORT = PORT process.env.DEEMIX_SERVER_PORT = PORT
process.env.DEEMIX_HOST = argv.host process.env.DEEMIX_HOST = argv.host
const server = new DeemixServer(argv.host, PORT)
server.init()
let win let win
const windowState = new WindowStateManager('mainWindow', { const windowState = new WindowStateManager('mainWindow', {
defaultWidth: 800, defaultWidth: 800,
@ -91,7 +93,7 @@ app.on('window-all-closed', () => {
}) })
ipcMain.on('openDownloadsFolder', (event)=>{ ipcMain.on('openDownloadsFolder', (event)=>{
const { downloadLocation } = server.getSettings().settings const { downloadLocation } = server.deemixApp.getSettings().settings
shell.openPath(downloadLocation) shell.openPath(downloadLocation)
}) })

View File

@ -1,2 +1 @@
bin/www
dist/ dist/

2
server/dist/app.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,426 +0,0 @@
/*!
* Connect - session - Cookie
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
*/
/*!
* Connect - session - Session
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
*/
/*!
* Connect - session - Store
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* MIT Licensed
*/
/*!
* Copyright (c) 2015, Salesforce.com, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of Salesforce.com nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*!
* Copyright (c) 2018, Salesforce.com, Inc.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. Neither the name of Salesforce.com nor the names of its contributors may
* be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*!
* Prototype.
*/
/*!
* accepts
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* basic-auth
* Copyright(c) 2013 TJ Holowaychuk
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* body-parser
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* body-parser
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* bytes
* Copyright(c) 2012-2014 TJ Holowaychuk
* Copyright(c) 2015 Jed Watson
* MIT Licensed
*/
/*!
* content-disposition
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* content-type
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* cookie
* Copyright(c) 2012-2014 Roman Shtylman
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* cookie-parser
* Copyright(c) 2014 TJ Holowaychuk
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* depd
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* depd
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* depd
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* depd
* Copyright(c) 2014-2018 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* depd
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* destroy
* Copyright(c) 2014 Jonathan Ong
* MIT Licensed
*/
/*!
* ee-first
* Copyright(c) 2014 Jonathan Ong
* MIT Licensed
*/
/*!
* encodeurl
* Copyright(c) 2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* escape-html
* Copyright(c) 2012-2013 TJ Holowaychuk
* Copyright(c) 2015 Andreas Lubbe
* Copyright(c) 2015 Tiancheng "Timothy" Gu
* MIT Licensed
*/
/*!
* etag
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* express
* Copyright(c) 2009-2013 TJ Holowaychuk
* Copyright(c) 2013 Roman Shtylman
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* express
* Copyright(c) 2009-2013 TJ Holowaychuk
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* express-session
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* express-session
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* finalhandler
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* forwarded
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* fresh
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2016-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* http-errors
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* media-typer
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* memorystore
* Copyright(c) 2020 Rocco Musolino <@roccomuso>
* MIT Licensed
*/
/*!
* merge-descriptors
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* methods
* Copyright(c) 2013-2014 TJ Holowaychuk
* Copyright(c) 2015-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* mime-db
* Copyright(c) 2014 Jonathan Ong
* MIT Licensed
*/
/*!
* mime-types
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* morgan
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* negotiator
* Copyright(c) 2012 Federico Romero
* Copyright(c) 2012-2014 Isaac Z. Schlueter
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* on-finished
* Copyright(c) 2013 Jonathan Ong
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* on-headers
* Copyright(c) 2014 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* parseurl
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* proxy-addr
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* random-bytes
* Copyright(c) 2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* range-parser
* Copyright(c) 2012-2014 TJ Holowaychuk
* Copyright(c) 2015-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* raw-body
* Copyright(c) 2013-2014 Jonathan Ong
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* send
* Copyright(c) 2012 TJ Holowaychuk
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* serve-static
* Copyright(c) 2010 Sencha Inc.
* Copyright(c) 2011 TJ Holowaychuk
* Copyright(c) 2014-2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* statuses
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* toidentifier
* Copyright(c) 2016 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* type-is
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2014-2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* uid-safe
* Copyright(c) 2014 Jonathan Ong
* Copyright(c) 2015-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* unpipe
* Copyright(c) 2015 Douglas Christopher Wilson
* MIT Licensed
*/
/*!
* vary
* Copyright(c) 2014-2017 Douglas Christopher Wilson
* MIT Licensed
*/
/*! http://mths.be/fromcodepoint v0.1.0 by @mathias */
/*! safe-buffer. MIT License. Feross Aboukhadijeh <https://feross.org/opensource> */

View File

@ -1,67 +1,374 @@
import http from 'http' import fs from 'fs'
import express, { Application } from 'express' import { sep } from 'path'
import { Server as WsServer } from 'ws' import { v4 as uuidv4 } from 'uuid'
import yargs from 'yargs' // @ts-expect-error
import initDebug from 'debug' import deemix from 'deemix'
import { hideBin } from 'yargs/helpers' import got from 'got'
import { Settings, Listener } from './types'
import { NotLoggedIn } from './helpers/errors'
import { registerMiddlewares } from './middlewares' import { GUI_PACKAGE } from './helpers/paths'
import indexRouter from './routes' // Types
const Downloader = deemix.downloader.Downloader
const { Single, Collection, Convertable } = deemix.types.downloadObjects
import { normalizePort } from './helpers/port' // Functions
import { getErrorCb, getListeningCb } from './helpers/server-callbacks' export const getAccessToken = deemix.utils.deezer.getAccessToken
import { registerApis } from './routes/api/register' export const getArlFromAccessToken = deemix.utils.deezer.getArlFromAccessToken
import { registerWebsocket } from './websocket'
import type { Arguments } from './types'
import { consoleInfo } from './helpers/errors'
export { getSettings } from './main' // 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
// TODO: Remove type assertion while keeping correct types export const sessionDZ: any = {}
const argv = yargs(hideBin(process.argv)).options({
port: { type: 'string', default: '6595' },
host: { type: 'string', default: '127.0.0.1' }
}).argv as Arguments
const DEEMIX_SERVER_PORT = normalizePort(process.env.DEEMIX_SERVER_PORT ?? argv.port) export class DeemixApp {
const DEEMIX_HOST = process.env.DEEMIX_HOST ?? argv.host queueOrder: string[]
queue: any
currentJob: any
const debug = initDebug('deemix-gui:server') deezerAvailable: boolean | null
export const wss = new WsServer({ noServer: true }) latestVersion: string | null
const app: Application = express()
const server = http.createServer(app)
/* === Middlewares === */ plugins: any
registerMiddlewares(app) settings: any
/* === Routes === */ listener: Listener
app.use('/', indexRouter)
/* === APIs === */ constructor(listener: Listener) {
registerApis(app) this.settings = deemix.settings.load(configFolder)
/* === Config === */ this.queueOrder = []
app.set('port', DEEMIX_SERVER_PORT) this.queue = {}
this.currentJob = null
/* === Server port === */ this.plugins = {
if (process.env.NODE_ENV !== 'test') { // eslint-disable-next-line new-cap
server.listen({ port: DEEMIX_SERVER_PORT, host: DEEMIX_HOST }) spotify: new deemix.plugins.spotify()
} }
this.deezerAvailable = null
this.latestVersion = null
this.listener = listener
registerWebsocket(wss) this.plugins.spotify.setup()
this.restoreQueueFromDisk()
}
/* === Server callbacks === */ async isDeezerAvailable(): Promise<boolean> {
app.on('mount', a => { if (this.deezerAvailable === null) {
console.log(a) let response
}) try {
server.on('connect', () => { response = await got.get('https://www.deezer.com/', {
consoleInfo('Server connected') headers: { Cookie: 'dz_lang=en; Domain=deezer.com; Path=/; Secure; hostOnly=false;' },
}) https: {
server.on('upgrade', (request, socket, head) => { rejectUnauthorized: false
wss.handleUpgrade(request, socket, head, socket => { },
wss.emit('connection', socket, request) retry: 5
}) })
}) } catch (e) {
server.on('error', getErrorCb(DEEMIX_SERVER_PORT)) console.trace(e)
server.on('listening', getListeningCb(server, debug)) this.deezerAvailable = false
return this.deezerAvailable
}
const title = (response.body.match(/<title[^>]*>([^<]+)<\/title>/)![1] || '').trim()
this.deezerAvailable = title !== 'Deezer will soon be available in your country.'
}
return this.deezerAvailable
}
async getLatestVersion(force = false): Promise<string | null> {
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) {
console.trace(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) {
console.trace(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.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]
console.log(`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) console.trace(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<any> {
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))
}
console.log(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
}
}
})
}
}

23
server/src/index.ts Normal file
View File

@ -0,0 +1,23 @@
import yargs from 'yargs'
import { hideBin } from 'yargs/helpers'
import type { Arguments } from './types'
import { DeemixServer } from './server'
const isModule = process.mainModule && process.mainModule.parent
if (!isModule) {
// TODO: Remove type assertion while keeping correct types
const argv = yargs(hideBin(process.argv)).options({
port: { type: 'string', default: '6595' },
host: { type: 'string', default: '127.0.0.1' }
}).argv as Arguments
const DEEMIX_SERVER_PORT = process.env.DEEMIX_SERVER_PORT ?? argv.port
const DEEMIX_HOST = process.env.DEEMIX_HOST ?? argv.host
const server = new DeemixServer(DEEMIX_HOST, DEEMIX_SERVER_PORT)
server.init()
}
export { DeemixServer }

View File

@ -1,362 +0,0 @@
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 got from 'got'
import { wss } from './app'
import { Settings } from './types'
import { NotLoggedIn } from './helpers/errors'
import { GUI_PACKAGE } from './helpers/paths'
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 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
let deezerAvailable: boolean | null = null
let latestVersion: string | null = null
export async function isDeezerAvailable(): Promise<boolean> {
if (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) {
console.trace(e)
deezerAvailable = false
return deezerAvailable
}
const title = (response.body.match(/<title[^>]*>([^<]+)<\/title>/)![1] || '').trim()
deezerAvailable = title !== 'Deezer will soon be available in your country.'
}
return deezerAvailable
}
export async function getLatestVersion(force = false): Promise<string | null> {
if ((latestVersion === null || force) && !settings.disableUpdateCheck) {
let response
try {
response = await got.get('https://deemix.app/gui/latest', {
https: {
rejectUnauthorized: false
}
})
} catch (e) {
console.trace(e)
latestVersion = 'NotFound'
return latestVersion
}
latestVersion = response.body.trim()
}
return latestVersion
}
function 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) {
console.trace(e)
return null
}
}
export function isUpdateAvailable(): boolean {
const currentVersionObj: any = parseVersion(currentVersion)
const latestVersionObj: any = parseVersion(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.commit !== currentVersionObj.commit) return true
return false
}
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) {
const logLine = deemix.utils.formatListener(key, data)
if (logLine) console.log(logLine)
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.getSettings() }
}
export function saveSettings(newSettings: any, newSpotifySettings: any) {
newSettings.executeCommand = settings.executeCommand
deemix.settings.save(newSettings, configFolder)
settings = newSettings
plugins.spotify.saveSettings(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[] = []
const downloadErrors: 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`)
let downloadObj
try {
downloadObj = await deemix.generateDownloadObject(dz, link, bitrate, plugins, 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) console.trace(e)
listener.send('queueError', { link: e.link, error: e.message, errid: e.errid })
})
}
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<any> {
do {
if (currentJob !== null || queueOrder.length === 0) {
// Should not start another download
return null
}
currentJob = true // lock currentJob
let currentUUID: string
do {
currentUUID = queueOrder.shift() || ''
} while (queue[currentUUID] === undefined && queueOrder.length)
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 && downloadObject.size !== 0) {
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') {
try {
queueOrder = JSON.parse(fs.readFileSync(configFolder + `queue${sep}order.json`).toString())
} catch {
queueOrder = []
fs.writeFileSync(configFolder + `queue${sep}order.json`, JSON.stringify(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
}
queue[downloadObject.uuid] = downloadObject.getEssentialDict()
queue[downloadObject.uuid].status = 'inQueue'
} else {
queue[currentItem.uuid] = currentItem
}
}
})
}

View File

@ -3,7 +3,7 @@ import type { RequestHandler } from 'express'
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import type { ApiHandler } from '../../../types' import type { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
export interface RawAlbumQuery { export interface RawAlbumQuery {
term: string term: string

View File

@ -5,7 +5,7 @@ import deemix from 'deemix'
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import type { ApiHandler, GetTrackResponse, GetAlbumResponse } from '../../../types' import type { ApiHandler, GetTrackResponse, GetAlbumResponse } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
export interface AnalyzeQuery { export interface AnalyzeQuery {
term?: string term?: string

View File

@ -1,13 +1,13 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { getLatestVersion, isUpdateAvailable } from '../../../main'
const path: ApiHandler['path'] = '/checkForUpdates' const path: ApiHandler['path'] = '/checkForUpdates'
const handler: ApiHandler['handler'] = async (_, res) => { const handler: ApiHandler['handler'] = async (req, res) => {
const latestCommit = await getLatestVersion() const deemix = req.app.get('deemix')
const latestCommit = await deemix.getLatestVersion()
res.send({ res.send({
latestCommit, latestCommit,
updateAvailable: isUpdateAvailable() updateAvailable: deemix.isUpdateAvailable()
}) })
} }

View File

@ -2,7 +2,7 @@ import { RequestHandler } from 'express'
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
import { isObjectEmpy } from '../../../helpers/primitive-checks' import { isObjectEmpy } from '../../../helpers/primitive-checks'
import { BadRequestError, isBadRequestError, consoleError } from '../../../helpers/errors' import { BadRequestError, isBadRequestError, consoleError } from '../../../helpers/errors'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getCharts' const path: ApiHandler['path'] = '/getCharts'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getHome' const path: ApiHandler['path'] = '/getHome'

View File

@ -1,13 +1,13 @@
// import { Deezer } from 'deezer-js' // import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { getQueue } from '../../../main'
const path: ApiHandler['path'] = '/getQueue' const path: ApiHandler['path'] = '/getQueue'
// let homeCache: any // let homeCache: any
const handler: ApiHandler['handler'] = (_, res) => { const handler: ApiHandler['handler'] = (req, res) => {
const result: any = getQueue() const deemix = req.app.get('deemix')
const result: any = deemix.getQueue()
res.send(result) res.send(result)
} }

View File

@ -1,10 +1,10 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { getSettings } from '../../../main'
const path: ApiHandler['path'] = '/getSettings' const path: ApiHandler['path'] = '/getSettings'
const handler: ApiHandler['handler'] = (_, res) => { const handler: ApiHandler['handler'] = (req, res) => {
res.send(getSettings()) const deemix = req.app.get('deemix')
res.send(deemix.getSettings())
} }
const apiHandler: ApiHandler = { path, handler } const apiHandler: ApiHandler = { path, handler }

View File

@ -1,13 +1,14 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ, plugins } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getTracklist' const path: ApiHandler['path'] = '/getTracklist'
const handler: ApiHandler['handler'] = async (req, res) => { const handler: ApiHandler['handler'] = async (req, res) => {
if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer() if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer()
const dz = sessionDZ[req.session.id] const dz = sessionDZ[req.session.id]
const deemix = req.app.get('deemix')
const list_id = String(req.query.id) const list_id = String(req.query.id)
const list_type = String(req.query.type) const list_type = String(req.query.type)
@ -20,7 +21,7 @@ const handler: ApiHandler['handler'] = async (req, res) => {
} }
case 'spotifyplaylist': case 'spotifyplaylist':
case 'spotify_playlist': { case 'spotify_playlist': {
if (!plugins.spotify.enabled) { if (!deemix.plugins.spotify.enabled) {
res.send({ res.send({
collaborative: false, collaborative: false,
description: '', description: '',
@ -40,7 +41,7 @@ const handler: ApiHandler['handler'] = async (req, res) => {
}) })
break break
} }
const sp = plugins.spotify.sp const sp = deemix.plugins.spotify.sp
let playlist = await sp.getPlaylist(list_id) let playlist = await sp.getPlaylist(list_id)
playlist = playlist.body playlist = playlist.body
let tracklist = playlist.tracks.items let tracklist = playlist.tracks.items

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getUserAlbums' const path: ApiHandler['path'] = '/getUserAlbums'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getUserArtists' const path: ApiHandler['path'] = '/getUserArtists'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getUserFavorites' const path: ApiHandler['path'] = '/getUserFavorites'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getUserPlaylists' const path: ApiHandler['path'] = '/getUserPlaylists'

View File

@ -1,13 +1,13 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { plugins } from '../../../main'
const path: ApiHandler['path'] = '/getUserSpotifyPlaylists' const path: ApiHandler['path'] = '/getUserSpotifyPlaylists'
const handler: ApiHandler['handler'] = async (req, res) => { const handler: ApiHandler['handler'] = async (req, res) => {
let data let data
const deemix = req.app.get('deemix')
if (plugins.spotify.enabled) { if (deemix.plugins.spotify.enabled) {
const sp = plugins.spotify.sp const sp = deemix.plugins.spotify.sp
const username = req.query.spotifyUser const username = req.query.spotifyUser
data = [] data = []
let playlists = await sp.getUserPlaylists(username) let playlists = await sp.getUserPlaylists(username)
@ -22,7 +22,7 @@ const handler: ApiHandler['handler'] = async (req, res) => {
playlistList = playlistList.concat(playlists.items) playlistList = playlistList.concat(playlists.items)
} }
playlistList.forEach((playlist: any) => { playlistList.forEach((playlist: any) => {
data.push(plugins.spotify._convertPlaylistStructure(playlist)) data.push(deemix.plugins.spotify._convertPlaylistStructure(playlist))
}) })
} else { } else {
data = { error: 'spotifyNotEnabled' } data = { error: 'spotifyNotEnabled' }

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/getUserTracks' const path: ApiHandler['path'] = '/getUserTracks'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/mainSearch' const path: ApiHandler['path'] = '/mainSearch'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
import { getAlbumDetails } from './albumSearch' import { getAlbumDetails } from './albumSearch'
const path: ApiHandler['path'] = '/newReleases' const path: ApiHandler['path'] = '/newReleases'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/search' const path: ApiHandler['path'] = '/search'

View File

@ -1,10 +1,10 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { plugins } from '../../../main'
const path: ApiHandler['path'] = '/spotifyStatus' const path: ApiHandler['path'] = '/spotifyStatus'
const handler: ApiHandler['handler'] = (_, res) => { const handler: ApiHandler['handler'] = (req, res) => {
res.send({ spotifyEnabled: plugins.spotify.enabled }) const deemix = req.app.get('deemix')
res.send({ spotifyEnabled: deemix.plugins.spotify.enabled })
} }
const apiHandler: ApiHandler = { path, handler } const apiHandler: ApiHandler = { path, handler }

View File

@ -1,26 +1,27 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ, addToQueue, getSettings, listener } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/addToQueue' const path: ApiHandler['path'] = '/addToQueue'
const handler: ApiHandler['handler'] = async (req, res) => { const handler: ApiHandler['handler'] = async (req, res) => {
if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer() if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer()
const deemix = req.app.get('deemix')
const dz = sessionDZ[req.session.id] const dz = sessionDZ[req.session.id]
const url = req.body.url.split(/[\s;]+/) const url = req.body.url.split(/[\s;]+/)
let bitrate = req.body.bitrate let bitrate = req.body.bitrate
if (bitrate === 'null' || bitrate === null) bitrate = getSettings().settings.maxBitrate if (bitrate === 'null' || bitrate === null) bitrate = deemix.getSettings().settings.maxBitrate
let obj: any let obj: any
try { try {
obj = await addToQueue(dz, url, bitrate) obj = await deemix.addToQueue(dz, url, bitrate)
} catch (e) { } catch (e) {
switch (e.name) { switch (e.name) {
case 'NotLoggedIn': case 'NotLoggedIn':
res.send({ result: false, errid: e.name, data: { url, bitrate } }) res.send({ result: false, errid: e.name, data: { url, bitrate } })
listener.send('loginNeededToDownload') deemix.listener.send('loginNeededToDownload')
break break
default: default:
console.error(e) console.error(e)

View File

@ -1,10 +1,10 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { cancelAllDownloads } from '../../../main'
const path = '/cancelAllDownloads' const path = '/cancelAllDownloads'
const handler: ApiHandler['handler'] = (_, res) => { const handler: ApiHandler['handler'] = (req, res) => {
cancelAllDownloads() const deemix = req.app.get('deemix')
deemix.cancelAllDownloads()
res.send({ result: true }) res.send({ result: true })
} }

View File

@ -3,7 +3,7 @@ import { RequestHandler } from 'express'
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/changeAccount' const path: ApiHandler['path'] = '/changeAccount'

View File

@ -1,7 +1,7 @@
import { RequestHandler } from 'express' import { RequestHandler } from 'express'
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { sessionDZ, startQueue, isDeezerAvailable } from '../../../main' import { sessionDZ } from '../../../app'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
export interface RawLoginArlBody { export interface RawLoginArlBody {
@ -21,6 +21,7 @@ const path: ApiHandler['path'] = '/loginArl'
const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async (req, res, _) => { const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async (req, res, _) => {
if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer() if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer()
const deemix = req.app.get('deemix')
const dz = sessionDZ[req.session.id] const dz = sessionDZ[req.session.id]
if (!req.body) { if (!req.body) {
@ -58,7 +59,7 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async (req, res, _)
response = await testDz.login_via_arl(...loginParams) response = await testDz.login_via_arl(...loginParams)
} }
if (response === LoginStatus.FAILED) sessionDZ[req.session.id] = new Deezer() if (response === LoginStatus.FAILED) sessionDZ[req.session.id] = new Deezer()
if (!(await isDeezerAvailable())) response = LoginStatus.NOT_AVAILABLE if (!(await deemix.isDeezerAvailable())) response = LoginStatus.NOT_AVAILABLE
const returnValue = { const returnValue = {
status: response, status: response,
arl: req.body.arl, arl: req.body.arl,
@ -67,7 +68,7 @@ const handler: RequestHandler<{}, {}, RawLoginArlBody, {}> = async (req, res, _)
currentChild: dz.selected_account currentChild: dz.selected_account
} }
if (response !== LoginStatus.NOT_AVAILABLE && response !== LoginStatus.FAILED) startQueue(dz) if (response !== LoginStatus.NOT_AVAILABLE && response !== LoginStatus.FAILED) deemix.startQueue(dz)
return res.status(200).send(returnValue) return res.status(200).send(returnValue)
} }

View File

@ -1,5 +1,5 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { getAccessToken, getArlFromAccessToken } from '../../../main' import { getAccessToken, getArlFromAccessToken } from '../../../app'
const path = '/loginEmail' const path = '/loginEmail'

View File

@ -1,7 +1,7 @@
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { sessionDZ } from '../../../main' import { sessionDZ } from '../../../app'
const path: ApiHandler['path'] = '/logout' const path: ApiHandler['path'] = '/logout'

View File

@ -1,10 +1,10 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { clearCompletedDownloads } from '../../../main'
const path = '/removeFinishedDownloads' const path = '/removeFinishedDownloads'
const handler: ApiHandler['handler'] = (_, res) => { const handler: ApiHandler['handler'] = (req, res) => {
clearCompletedDownloads() const deemix = req.app.get('deemix')
deemix.clearCompletedDownloads()
res.send({ result: true }) res.send({ result: true })
} }

View File

@ -1,12 +1,12 @@
import { ApiHandler } from '../../../types' import { ApiHandler } from '../../../types'
import { cancelDownload } from '../../../main'
const path = '/removeFromQueue' const path = '/removeFromQueue'
const handler: ApiHandler['handler'] = (req, res) => { const handler: ApiHandler['handler'] = (req, res) => {
const deemix = req.app.get('deemix')
const { uuid } = req.query const { uuid } = req.query
if (uuid) { if (uuid) {
cancelDownload(uuid) deemix.cancelDownload(uuid)
res.send({ result: true }) res.send({ result: true })
} else { } else {
res.send({ result: false }) res.send({ result: false })

View File

@ -1,5 +1,4 @@
import { ApiHandler, Settings, SpotifySettings } from '../../../types' import { ApiHandler, Settings, SpotifySettings } from '../../../types'
import { saveSettings, listener } from '../../../main'
const path = '/saveSettings' const path = '/saveSettings'
@ -9,9 +8,10 @@ export interface SaveSettingsData {
} }
const handler: ApiHandler['handler'] = (req, res) => { const handler: ApiHandler['handler'] = (req, res) => {
const deemix = req.app.get('deemix')
const { settings, spotifySettings }: SaveSettingsData = req.query const { settings, spotifySettings }: SaveSettingsData = req.query
saveSettings(settings, spotifySettings) deemix.saveSettings(settings, spotifySettings)
listener.send('updateSettings', { settings, spotifySettings }) deemix.listener.send('updateSettings', { settings, spotifySettings })
res.send({ result: true }) res.send({ result: true })
} }

View File

@ -2,7 +2,7 @@ import express from 'express'
// @ts-expect-error // @ts-expect-error
import { Deezer } from 'deezer-js' import { Deezer } from 'deezer-js'
import { consoleInfo } from '../helpers/errors' import { consoleInfo } from '../helpers/errors'
import { sessionDZ, getQueue, deemixVersion, currentVersion, isDeezerAvailable, plugins, getSettings } from '../main' import { sessionDZ, deemixVersion, currentVersion } from '../app'
const router = express.Router() const router = express.Router()
let update: any = null let update: any = null
@ -10,6 +10,7 @@ let update: any = null
router.get('/connect', async (req, res) => { router.get('/connect', async (req, res) => {
if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer() if (!sessionDZ[req.session.id]) sessionDZ[req.session.id] = new Deezer()
const dz = sessionDZ[req.session.id] const dz = sessionDZ[req.session.id]
const deemix = req.app.get('deemix')
if (!update) { if (!update) {
consoleInfo(`Currently running deemix-gui version ${currentVersion}`) consoleInfo(`Currently running deemix-gui version ${currentVersion}`)
@ -24,14 +25,14 @@ router.get('/connect', async (req, res) => {
update, update,
autologin: !dz.logged_in, autologin: !dz.logged_in,
currentUser: dz.current_user, currentUser: dz.current_user,
deezerAvailable: await isDeezerAvailable(), deezerAvailable: await deemix.isDeezerAvailable(),
spotifyEnabled: plugins.spotify.enabled, spotifyEnabled: deemix.plugins.spotify.enabled,
settingsData: getSettings() settingsData: deemix.getSettings()
} }
if (result.settingsData.settings.autoCheckForUpdates) result.checkForUpdates = true if (result.settingsData.settings.autoCheckForUpdates) result.checkForUpdates = true
const queue = getQueue() const queue = deemix.getQueue()
if (Object.keys(queue.queue).length > 0) { if (Object.keys(queue.queue).length > 0) {
result.queue = queue result.queue = queue

88
server/src/server.ts Normal file
View File

@ -0,0 +1,88 @@
import http, { Server } from 'http'
import express, { Application } from 'express'
import { Server as WsServer } from 'ws'
import initDebug from 'debug'
// @ts-expect-error
import deemix from 'deemix'
import { registerMiddlewares } from './middlewares'
import indexRouter from './routes'
import { getErrorCb, getListeningCb } from './helpers/server-callbacks'
import { registerApis } from './routes/api/register'
import { registerWebsocket } from './websocket'
import { consoleInfo } from './helpers/errors'
import { Port, Listener } from './types'
import { DeemixApp } from './app'
import { normalizePort } from './helpers/port'
export class DeemixServer {
host: string
port: Port
wss: WsServer
app: Application
server: Server
deemixApp: DeemixApp
constructor(host: string, port: string) {
this.host = host
this.port = normalizePort(port)
this.wss = new WsServer({ noServer: true })
this.app = express()
this.server = http.createServer(this.app)
const listener: Listener = {
send: (key: string, data?: any) => {
const logLine = deemix.utils.formatListener(key, data)
if (logLine) console.log(logLine)
if (['downloadInfo', 'downloadWarn'].includes(key)) return
this.wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ key, data }))
}
})
}
}
this.deemixApp = new DeemixApp(listener)
}
init() {
const debug = initDebug('deemix-gui:server')
this.app.set('deemix', this.deemixApp)
/* === Middlewares === */
registerMiddlewares(this.app)
/* === Routes === */
this.app.use('/', indexRouter)
/* === APIs === */
registerApis(this.app)
/* === Config === */
this.app.set('port', this.port)
/* === Server port === */
if (process.env.NODE_ENV !== 'test') {
this.server.listen({ port: this.port, host: this.host })
}
registerWebsocket(this.wss, this.deemixApp)
/* === Server callbacks === */
this.app.on('mount', a => {
console.log(a)
})
this.server.on('connect', () => {
consoleInfo('Server connected')
})
this.server.on('upgrade', (request, socket, head) => {
this.wss.handleUpgrade(request, socket, head, socket => {
this.wss.emit('connection', socket, request)
})
})
this.server.on('error', getErrorCb(this.port))
this.server.on('listening', getListeningCb(this.server, debug))
}
}

View File

@ -239,3 +239,7 @@ export interface Arguments {
[x: string]: unknown [x: string]: unknown
$0: string $0: string
} }
export interface Listener {
send: (key: string, data?: any) => void
}

View File

@ -1,19 +1,17 @@
import { Server as WsServer } from 'ws' import { Server as WsServer } from 'ws'
import { consoleError, consoleInfo } from '../helpers/errors' import { consoleError, consoleInfo } from '../helpers/errors'
import { DeemixApp } from '../app'
import wsModules from './modules' import wsModules from './modules'
// ? Is this needed? export const registerWebsocket = (wss: WsServer, deemix: DeemixApp) => {
// ? https://github.com/websockets/ws#how-to-detect-and-close-broken-connections
export const registerWebsocket = (wss: WsServer) => {
wss.on('connection', ws => { wss.on('connection', ws => {
ws.on('message', message => { ws.on('message', message => {
const data = JSON.parse(message.toString()) const data = JSON.parse(message.toString())
wsModules.forEach(module => { wsModules.forEach(module => {
if (data.key === module.eventName) { if (data.key === module.eventName) {
module.cb(data.data, ws, wss) module.cb(data.data, ws, wss, deemix)
} }
}) })
}) })

View File

@ -1,11 +1,11 @@
import { Server as WsServer } from 'ws' import { Server as WsServer } from 'ws'
import { consoleInfo } from '../../helpers/errors' import { consoleInfo } from '../../helpers/errors'
import { cancelAllDownloads } from '../../main' import { DeemixApp } from '../../app'
const eventName = 'cancelAllDownloads' const eventName = 'cancelAllDownloads'
const cb = (_: any, __: any, ___: WsServer) => { const cb = (_: any, __: any, ___: WsServer, deemix: DeemixApp) => {
cancelAllDownloads() deemix.cancelAllDownloads()
consoleInfo(`Queue cleared`) consoleInfo(`Queue cleared`)
} }

View File

@ -1,11 +1,11 @@
import { Server as WsServer } from 'ws' import { Server as WsServer } from 'ws'
import { consoleInfo } from '../../helpers/errors' import { consoleInfo } from '../../helpers/errors'
import { clearCompletedDownloads } from '../../main' import { DeemixApp } from '../../app'
const eventName = 'removeFinishedDownloads' const eventName = 'removeFinishedDownloads'
const cb = (_: any, __: any, ___: WsServer) => { const cb = (_: any, __: any, ___: WsServer, deemix: DeemixApp) => {
clearCompletedDownloads() deemix.clearCompletedDownloads()
consoleInfo('Completed downloads cleared') consoleInfo('Completed downloads cleared')
} }

View File

@ -1,11 +1,11 @@
import { Server as WsServer } from 'ws' import { Server as WsServer } from 'ws'
import { consoleInfo } from '../../helpers/errors' import { consoleInfo } from '../../helpers/errors'
import { cancelDownload } from '../../main' import { DeemixApp } from '../../app'
const eventName = 'removeFromQueue' const eventName = 'removeFromQueue'
const cb = (data: any, __: any, ___: WsServer) => { const cb = (data: any, __: any, ___: WsServer, deemix: DeemixApp) => {
cancelDownload(data) deemix.cancelDownload(data)
consoleInfo(`Cancelled ${data}`) consoleInfo(`Cancelled ${data}`)
} }

View File

@ -1,6 +1,6 @@
import { Server as WsServer } from 'ws' import { Server as WsServer } from 'ws'
import { consoleInfo } from '../../helpers/errors' import { consoleInfo } from '../../helpers/errors'
import { saveSettings, listener } from '../../main' import { DeemixApp } from '../../app'
import { Settings, SpotifySettings } from '../../types' import { Settings, SpotifySettings } from '../../types'
const eventName = 'saveSettings' const eventName = 'saveSettings'
@ -10,11 +10,11 @@ export interface SaveSettingsData {
spotifySettings: SpotifySettings spotifySettings: SpotifySettings
} }
const cb = (data: SaveSettingsData, _: any, __: WsServer) => { const cb = (data: SaveSettingsData, _: any, __: WsServer, deemix: DeemixApp) => {
const { settings, spotifySettings } = data const { settings, spotifySettings } = data
saveSettings(settings, spotifySettings) deemix.saveSettings(settings, spotifySettings)
consoleInfo('Settings saved') consoleInfo('Settings saved')
listener.send('updateSettings', { settings, spotifySettings }) deemix.listener.send('updateSettings', { settings, spotifySettings })
} }
export default { eventName, cb } export default { eventName, cb }

View File

@ -6,7 +6,7 @@ module.exports = env => {
const isProduction = !!env.production const isProduction = !!env.production
const config = { const config = {
mode: isProduction ? 'production' : 'development', mode: isProduction ? 'production' : 'development',
entry: './src/app.ts', entry: './src/index.ts',
devtool: isProduction ? false : 'eval', devtool: isProduction ? false : 'eval',
module: { module: {
rules: [ rules: [

2
webui

@ -1 +1 @@
Subproject commit 5fb5ae4ace6f271bee3cd744208197549738897d Subproject commit c4cd5cb3b8cb49676d9e19761324a037cee3eab5