diff --git a/app.py b/app.py index 089faef..c41f935 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,7 @@ #!/usr/bin/env python3 +import eventlet +requests = eventlet.import_patched('requests') + from deemix.api.deezer import Deezer from deemix.app.settings import Settings from deemix.app.queuemanager import QueueManager @@ -10,6 +13,35 @@ from deemix.utils.localpaths import getConfigFolder import os.path as path import json +from datetime import datetime + +def resource_path(relative_path): + """ Get absolute path to resource, works for dev and for PyInstaller """ + try: + # PyInstaller creates a temp folder and stores path in _MEIPASS + base_path = sys._MEIPASS + except Exception: + base_path = path.dirname(path.abspath(path.realpath(__file__))) + + return path.join(base_path, relative_path) + +class LoginStatus(): + """Login status codes""" + + NOT_AVAILABLE = -1 + """Deezer is not Available in your country""" + + FAILED = 0 + """Login Failed""" + + SUCCESS = 1 + """Login Successfull""" + + ALREADY_LOGGED = 2 + """Already logged in""" + + FORCED_SUCCESS = 3 + """Forced Login Successfull""" class deemix: def __init__(self, portable): @@ -21,6 +53,50 @@ class deemix: self.chartsList = [] self.homeCache = None + self.currentVersion = None + self.latestVersion = None + self.updateAvailable = False + self.isDeezerAvailable = True + + def checkForUpdates(self): + commitFile = resource_path('version.txt') + if path.isfile(commitFile): + print("Checking for updates...") + with open(commitFile, 'r') as f: + self.currentVersion = f.read().strip() + try: + latestVersion = requests.get("https://deemix.app/pyweb/latest") + latestVersion.raise_for_status() + self.latestVersion = latestVersion.text.strip() + except: + self.latestVersion = None + self.updateAvailable = self.compare_versions() + if self.updateAvailable: + print("Update available! Commit: "+self.latestVersion) + else: + print("You're running the latest version") + + def compareVersions(): + if not self.latestVersion or not self.currentVersion: + return False + (currentDate, currentCommit) = tuple(self.currentVersion.split('-')) + (latestDate, latestCommit) = tuple(self.latestVersion.split('-')) + currentDate = currentDate.split('.') + latestDate = latestDate.split('.') + current = datetime(int(currentDate[0]), int(currentDate[1]), int(currentDate[2])) + latest = datetime(int(latestDate[0]), int(latestDate[1]), int(latestDate[2])) + if latest > current: + return True + elif latest == current: + return latestCommit != currentCommit + else: + return False + + def checkDeezerAvailability(self): + body = requests.get("https://www.deezer.com/", headers={'Cookie': 'dz_lang=en; Domain=deezer.com; Path=/; Secure; hostOnly=false;'}).text + title = body[body.find('')+7:body.find('')] + self.isDeezerAvailable = title.strip() != "Deezer will soon be available in your country." + def shutdown(self, interface=None): if self.set.settings['saveDownloadQueue']: self.qm.saveQueue(self.configFolder) @@ -43,6 +119,11 @@ class deemix: f.write(arl) return arl + def login(self, dz, arl, child): + if not dz.logged_in: + return int(dz.login_via_arl(arl, child)) + else: + return LoginStatus.ALREADY_LOGGED def restoreDownloadQueue(self, dz, interface=None): self.qm.loadQueue(self.configFolder, self.set.settings, interface) @@ -149,24 +230,22 @@ class deemix: url = url.split(";") self.qm.addToQueue(dz, url, self.set.settings, bitrate, interface, ack) - def removeFromQueue(self, uuid, interface=None): self.qm.removeFromQueue(uuid, interface) - def cancelAllDownloads(self, interface=None): self.qm.cancelAllDownloads(interface) - def removeFinishedDownloads(self, interface=None): self.qm.removeFinishedDownloads(interface) - def initDownloadQueue(self): (queue, queueComplete, queueList, currentItem) = self.qm.getQueue() return (queue, queueComplete, queueList, currentItem) def analyzeLink(self, dz, link): + if 'deezer.page.link' in link: + link = requests.get(link).url type = getTypeFromLink(link) relID = getIDFromLink(link, type) if type in ["track", "album"]: diff --git a/deemix-pyweb.py b/deemix-pyweb.py index c2d5c40..f477ec0 100644 --- a/deemix-pyweb.py +++ b/deemix-pyweb.py @@ -227,7 +227,7 @@ if __name__ == '__main__': configFolder = portable or getConfigFolder() x,y,w,h = get_position() window = MainWindow('deemix', 'http://'+url+':'+str(port), x,y,w,h) - t = Thread(target=run_server, args=(port, url, portable, window)) + t = Thread(target=run_server, args=(url, port, portable, window)) else: t = Thread(target=run_server, args=(port, url, portable)) t.daemon = True diff --git a/server.py b/server.py index 496d378..dc79c9c 100644 --- a/server.py +++ b/server.py @@ -9,16 +9,13 @@ import eventlet from eventlet import tpool from eventlet.green import subprocess requests = eventlet.import_patched('requests') -urlopen = eventlet.import_patched('urllib.request').urlopen - -from datetime import datetime from flask import Flask, render_template, request, session, redirect, copy_current_request_context from flask_socketio import SocketIO, emit from werkzeug.middleware.proxy_fix import ProxyFix -from deemix import __version__ as deemixVersion -from app import deemix +from deemix import __version__ as deemix_version +from app import deemix, LoginStatus, resource_path from deemix.api.deezer import Deezer from deemix.app.messageinterface import MessageInterface @@ -33,10 +30,21 @@ mimetypes.add_type('text/javascript', '.js') from engineio.payload import Payload Payload.max_decode_packets = 500 -app = None -gui = None -arl = None +# Disable logging +serverLog = logging.getLogger('werkzeug') +serverLog.disabled = True +logging.getLogger('socketio').setLevel(logging.ERROR) +logging.getLogger('engineio').setLevel(logging.ERROR) +#server.logger.disabled = True +class SocketInterface(MessageInterface): + def send(self, message, value=None): + if value: + socketio.emit(message, value) + else: + socketio.emit(message) + +# This allows the frontend to use vue.js class CustomFlask(Flask): jinja_options = Flask.jinja_options.copy() jinja_options.update(dict( @@ -48,99 +56,31 @@ class CustomFlask(Flask): comment_end_string='#$', )) -def resource_path(relative_path): - """ Get absolute path to resource, works for dev and for PyInstaller """ - try: - # PyInstaller creates a temp folder and stores path in _MEIPASS - base_path = sys._MEIPASS - except Exception: - base_path = path.dirname(path.abspath(path.realpath(__file__))) - - return path.join(base_path, relative_path) - -gui_dir = resource_path(path.join('webui', 'public')) -if not path.exists(gui_dir): - gui_dir = resource_path('webui') -if not path.isfile(path.join(gui_dir, 'index.html')): +# Retrocompatibility with old versions of the app +# Check for public folder and fallback to webui +GUI_DIR = resource_path(path.join('webui', 'public')) +if not path.exists(GUI_DIR): + GUI_DIR = resource_path('webui') +if not path.isfile(path.join(GUI_DIR, 'index.html')): sys.exit("WebUI not found, please download and add a WebUI") -server = CustomFlask(__name__, static_folder=gui_dir, template_folder=gui_dir, static_url_path="") + +server = CustomFlask(__name__, static_folder=GUI_DIR, template_folder=GUI_DIR, static_url_path="") server.config['SEND_FILE_MAX_AGE_DEFAULT'] = 1 # disable caching socketio = SocketIO(server) server.wsgi_app = ProxyFix(server.wsgi_app, x_for=1, x_proto=1) -class SocketInterface(MessageInterface): - def send(self, message, value=None): - if value: - socketio.emit(message, value) - else: - socketio.emit(message) - +app = None +gui = None +arl = None socket_interface = SocketInterface() -loginWindow = False +first_connection = True -serverLog = logging.getLogger('werkzeug') -serverLog.disabled = True -logging.getLogger('socketio').setLevel(logging.ERROR) -logging.getLogger('engineio').setLevel(logging.ERROR) -#server.logger.disabled = True - -firstConnection = True - -currentVersion = None -latestVersion = None -updateAvailable = False - -def compare_versions(currentVersion, latestVersion): - if not latestVersion or not currentVersion: - return False - (currentDate, currentCommit) = tuple(currentVersion.split('-')) - (latestDate, latestCommit) = tuple(latestVersion.split('-')) - currentDate = currentDate.split('.') - latestDate = latestDate.split('.') - current = datetime(int(currentDate[0]), int(currentDate[1]), int(currentDate[2])) - latest = datetime(int(latestDate[0]), int(latestDate[1]), int(latestDate[2])) - if latest > current: - return True - elif latest == current: - return latestCommit != currentCommit - else: - return False - -def check_for_updates(): - global currentVersion, latestVersion, updateAvailable - commitFile = resource_path('version.txt') - if path.isfile(commitFile): - print("Checking for updates...") - with open(commitFile, 'r') as f: - currentVersion = f.read().strip() - try: - latestVersion = requests.get("https://deemix.app/pyweb/latest") - latestVersion.raise_for_status() - latestVersion = latestVersion.text.strip() - except: - latestVersion = None - if currentVersion and latestVersion: - updateAvailable = compare_versions(currentVersion, latestVersion) - if updateAvailable: - print("Update available! Commit: "+latestVersion) - else: - print("You're running the latest version") - -is_deezer_available = True -def check_deezer_availability(): - body = requests.get("https://www.deezer.com/", headers={'Cookie': 'dz_lang=en; Domain=deezer.com; Path=/; Secure; hostOnly=false;'}).text - title = body[body.find('')+7:body.find('')] - return title.strip() != "Deezer will soon be available in your country." - -def shutdown(interface=None): +def shutdown(): if app is not None: - app.shutdown(interface=interface) + app.shutdown(socket_interface) socketio.stop() -def shutdown_handler(signalnum, frame): - shutdown() - @server.route('/') def landing(): return render_template('index.html') @@ -151,27 +91,29 @@ def not_found_handler(e): @server.route('/shutdown') def closing(): - shutdown(interface=socket_interface) + shutdown() return 'Server Closed' -serverwide_arl = "--serverwide-arl" in sys.argv -if serverwide_arl: - print("Server-wide ARL enabled.") - @socketio.on('connect') def on_connect(): session['dz'] = Deezer() (settings, spotifyCredentials, defaultSettings) = app.getAllSettings() session['dz'].set_accept_language(settings.get('tagsLanguage')) emit('init_settings', (settings, spotifyCredentials, defaultSettings)) - emit('init_update', - {'currentCommit': currentVersion, - 'latestCommit': latestVersion, - 'updateAvailable': updateAvailable, - 'deemixVersion': deemixVersion} + + if first_connection: + app.checkForUpdates() + app.checkDeezerAvailability() + + emit('init_update',{ + 'currentCommit': app.currentVersion, + 'latestCommit': app.latestVersion, + 'updateAvailable': app.updateAvailable, + 'deemixVersion': deemix_version + } ) - if serverwide_arl: + if arl: login(arl) else: emit('init_autologin') @@ -184,9 +126,11 @@ def on_connect(): 'queueList': queueList, 'currentItem': currentItem }) + emit('init_home', app.get_home(session['dz'])) emit('init_charts', app.get_charts(session['dz'])) - if not is_deezer_available: + + if not app.isDeezerAvailable: emit('deezerNotAvailable') @socketio.on('get_home_data') @@ -207,49 +151,38 @@ def get_settings_data(): @socketio.on('login') def login(arl, force=False, child=0): - global firstConnection, is_deezer_available - if not is_deezer_available: - emit('logged_in', {'status': -1, 'arl': arl, 'user': session['dz'].user}) + global first_connection + + if not app.isDeezerAvailable: + emit('logged_in', {'status': LoginStatus.NOT_AVAILABLE, 'arl': arl, 'user': session['dz'].user}) return - if child == None: - child = 0 + + if child == None: child = 0 arl = arl.strip() emit('logging_in') - if not session['dz'].logged_in: - result = session['dz'].login_via_arl(arl, int(child)) - else: - if force: - session['dz'] = Deezer() - result = session['dz'].login_via_arl(arl, int(child)) - if result == 1: - result = 3 - else: - result = 2 + + if force: session['dz'] = Deezer() + result = app.login(session['dz'], arl, int(child)) + if force and result == LoginStatus.SUCCESS: result = LoginStatus.FORCED_SUCCESS + emit('logged_in', {'status': result, 'arl': arl, 'user': session['dz'].user}) - if firstConnection and result in [1, 3]: - firstConnection = False + if first_connection and result in [LoginStatus.SUCCESS, LoginStatus.FORCED_SUCCESS]: + first_connection = False app.restoreDownloadQueue(session['dz'], socket_interface) if result != 0: emit('familyAccounts', session['dz'].childs) emit('init_favorites', app.getUserFavorites(session['dz'])) - @socketio.on('changeAccount') def changeAccount(child): emit('accountChanged', session['dz'].change_account(int(child))) emit('init_favorites', app.getUserFavorites(session['dz'])) - @socketio.on('logout') def logout(): - status = 0 if session['dz'].logged_in: session['dz'] = Deezer() - status = 0 - else: - status = 1 - emit('logged_out', status) - + emit('logged_out') @socketio.on('mainSearch') def mainSearch(data): @@ -258,7 +191,6 @@ def mainSearch(data): result['ack'] = data.get('ack') emit('mainSearch', result) - @socketio.on('search') def search(data): if data['term'].strip() != "": @@ -292,29 +224,22 @@ def newReleases(data): def queueRestored(): app.queueRestored(session['dz'], socket_interface) - @socketio.on('addToQueue') def addToQueue(data): result = app.addToQueue(session['dz'], data['url'], data['bitrate'], interface=socket_interface, ack=data.get('ack')) - if result == "Not logged in": - emit('loginNeededToDownload') - @socketio.on('removeFromQueue') def removeFromQueue(uuid): app.removeFromQueue(uuid, interface=socket_interface) - @socketio.on('removeFinishedDownloads') def removeFinishedDownloads(): app.removeFinishedDownloads(interface=socket_interface) - @socketio.on('cancelAllDownloads') def cancelAllDownloads(): app.cancelAllDownloads(interface=socket_interface) - @socketio.on('saveSettings') def saveSettings(settings, spotifyCredentials, spotifyUser): app.saveSettings(settings, session['dz']) @@ -323,7 +248,6 @@ def saveSettings(settings, spotifyCredentials, spotifyUser): if spotifyUser != False: emit('updated_userSpotifyPlaylists', app.updateUserSpotifyPlaylists(spotifyUser)) - @socketio.on('getTracklist') def getTracklist(data): if data['type'] == 'artist': @@ -355,8 +279,6 @@ def getTracklist(data): @socketio.on('analyzeLink') def analyzeLink(link): - if 'deezer.page.link' in link: - link = urlopen(link).url (type, data) = app.analyzeLink(session['dz'], link) if len(data): emit('analyze_'+type, data) @@ -411,7 +333,7 @@ def selectDownloadFolder(): emit('downloadFolderSelected', result) else: print("Can't open folder selection, you're not running the gui") - + def doSelectDowloadFolder(): gui.selectDownloadFolder_trigger.emit() gui._selectDownloadFolder_semaphore.acquire() @@ -429,41 +351,42 @@ def applogin(): emit('logged_in', {'status': 2, 'user': session['dz'].user}) else: print("Can't open login page, you're not running the gui") - + def dologin(): gui.appLogin_trigger.emit() gui._appLogin_semaphore.acquire() return gui.arl -def run_server(port, host="127.0.0.1", portable=None, mainWindow=None): - global app, gui, arl, is_deezer_available +def run_server(host="127.0.0.1", port=6595, portable=None, guiWindow=None, server_arl=False): + global app, gui, arl app = deemix(portable) - gui = mainWindow - if serverwide_arl: + gui = guiWindow + if server_arl: + print("Server-wide ARL enabled.") arl = app.getConfigArl() - check_for_updates() - if not check_deezer_availability(): - is_deezer_available = False - print("Deezer is not available in your country, you should use a VPN to use this app.") print("Starting server at http://" + host + ":" + str(port)) socketio.run(server, host=host, port=port) +def shutdown_handler(signalnum, frame): + shutdown() if __name__ == '__main__': - port = 6595 host = "127.0.0.1" - portable = None + port = 6595 if len(sys.argv) >= 2: try: port = int(sys.argv[1]) except ValueError: pass + + portable = None if '--portable' in sys.argv: portable = path.join(path.dirname(path.realpath(__file__)), 'config') if '--host' in sys.argv: host = str(sys.argv[sys.argv.index("--host")+1]) + serverwide_arl = "--serverwide-arl" in sys.argv signal.signal(signal.SIGINT, shutdown_handler) signal.signal(signal.SIGTERM, shutdown_handler) - run_server(port, host, portable) + run_server(host, port, portable, server_arl=serverwide_arl)