The pywebview app for deemix-webui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

435 lines
14 KiB

  1. #!/usr/bin/env python3
  2. import logging
  3. import signal
  4. import sys
  5. import subprocess
  6. from os import path
  7. import json
  8. import eventlet
  9. requests = eventlet.import_patched('requests')
  10. urlopen = eventlet.import_patched('urllib.request').urlopen
  11. from datetime import datetime
  12. from flask import Flask, render_template, request, session, redirect, copy_current_request_context
  13. from flask_socketio import SocketIO, emit
  14. from werkzeug.middleware.proxy_fix import ProxyFix
  15. from deemix import __version__ as deemixVersion
  16. from app import deemix
  17. from deemix.api.deezer import Deezer
  18. from deemix.app.messageinterface import MessageInterface
  19. # Workaround for MIME type error in certain Windows installs
  20. # https://github.com/pallets/flask/issues/1045#issuecomment-42202749
  21. import mimetypes
  22. mimetypes.add_type('text/css', '.css')
  23. mimetypes.add_type('text/javascript', '.js')
  24. # Makes engineio accept more packets from client, needed for long URL lists in addToQueue requests
  25. # https://github.com/miguelgrinberg/python-engineio/issues/142#issuecomment-545807047
  26. from engineio.payload import Payload
  27. Payload.max_decode_packets = 500
  28. app = None
  29. gui = None
  30. arl = None
  31. class CustomFlask(Flask):
  32. jinja_options = Flask.jinja_options.copy()
  33. jinja_options.update(dict(
  34. block_start_string='$$',
  35. block_end_string='$$',
  36. variable_start_string='$',
  37. variable_end_string='$',
  38. comment_start_string='$#',
  39. comment_end_string='#$',
  40. ))
  41. def resource_path(relative_path):
  42. """ Get absolute path to resource, works for dev and for PyInstaller """
  43. try:
  44. # PyInstaller creates a temp folder and stores path in _MEIPASS
  45. base_path = sys._MEIPASS
  46. except Exception:
  47. base_path = path.dirname(path.abspath(path.realpath(__file__)))
  48. return path.join(base_path, relative_path)
  49. gui_dir = resource_path(path.join('webui', 'public'))
  50. if not path.exists(gui_dir):
  51. gui_dir = resource_path('webui')
  52. if not path.isfile(path.join(gui_dir, 'index.html')):
  53. sys.exit("WebUI not found, please download and add a WebUI")
  54. server = CustomFlask(__name__, static_folder=gui_dir, template_folder=gui_dir, static_url_path="")
  55. server.config['SEND_FILE_MAX_AGE_DEFAULT'] = 1 # disable caching
  56. socketio = SocketIO(server)
  57. server.wsgi_app = ProxyFix(server.wsgi_app, x_for=1, x_proto=1)
  58. class SocketInterface(MessageInterface):
  59. def send(self, message, value=None):
  60. if value:
  61. socketio.emit(message, value)
  62. else:
  63. socketio.emit(message)
  64. socket_interface = SocketInterface()
  65. loginWindow = False
  66. serverLog = logging.getLogger('werkzeug')
  67. serverLog.disabled = True
  68. logging.getLogger('socketio').setLevel(logging.ERROR)
  69. logging.getLogger('engineio').setLevel(logging.ERROR)
  70. #server.logger.disabled = True
  71. firstConnection = True
  72. currentVersion = None
  73. latestVersion = None
  74. updateAvailable = False
  75. def compare_versions(currentVersion, latestVersion):
  76. if not latestVersion or not currentVersion:
  77. return False
  78. (currentDate, currentCommit) = tuple(currentVersion.split('-'))
  79. (latestDate, latestCommit) = tuple(latestVersion.split('-'))
  80. currentDate = currentDate.split('.')
  81. latestDate = latestDate.split('.')
  82. current = datetime(int(currentDate[0]), int(currentDate[1]), int(currentDate[2]))
  83. latest = datetime(int(latestDate[0]), int(latestDate[1]), int(latestDate[2]))
  84. if latest > current:
  85. return True
  86. elif latest == current:
  87. return latestCommit != currentCommit
  88. else:
  89. return False
  90. def check_for_updates():
  91. global currentVersion, latestVersion, updateAvailable
  92. commitFile = resource_path('version.txt')
  93. if path.isfile(commitFile):
  94. print("Checking for updates...")
  95. with open(commitFile, 'r') as f:
  96. currentVersion = f.read().strip()
  97. try:
  98. latestVersion = requests.get("https://deemix.app/pyweb/latest")
  99. latestVersion.raise_for_status()
  100. latestVersion = latestVersion.text.strip()
  101. except:
  102. latestVersion = None
  103. if currentVersion and latestVersion:
  104. updateAvailable = compare_versions(currentVersion, latestVersion)
  105. if updateAvailable:
  106. print("Update available! Commit: "+latestVersion)
  107. else:
  108. print("You're running the latest version")
  109. is_deezer_available = True
  110. def check_deezer_availability():
  111. body = requests.get("https://www.deezer.com/", headers={'Cookie': 'dz_lang=en; Domain=deezer.com; Path=/; Secure; hostOnly=false;'}).text
  112. title = body[body.find('<title>')+7:body.find('</title>')]
  113. return title.strip() != "Deezer will soon be available in your country."
  114. def shutdown(interface=None):
  115. if app is not None:
  116. app.shutdown(interface=interface)
  117. socketio.stop()
  118. def shutdown_handler(signalnum, frame):
  119. shutdown()
  120. @server.route('/')
  121. def landing():
  122. return render_template('index.html')
  123. @server.errorhandler(404)
  124. def not_found_handler(e):
  125. return redirect("/")
  126. @server.route('/shutdown')
  127. def closing():
  128. shutdown(interface=socket_interface)
  129. return 'Server Closed'
  130. serverwide_arl = "--serverwide-arl" in sys.argv
  131. if serverwide_arl:
  132. print("Server-wide ARL enabled.")
  133. @socketio.on('connect')
  134. def on_connect():
  135. session['dz'] = Deezer()
  136. settings = app.getSettings()
  137. session['dz'].set_accept_language(settings.get('tagsLanguage'))
  138. spotifyCredentials = app.getSpotifyCredentials()
  139. defaultSettings = app.getDefaultSettings()
  140. emit('init_settings', (settings, spotifyCredentials, defaultSettings))
  141. emit('init_update',
  142. {'currentCommit': currentVersion,
  143. 'latestCommit': latestVersion,
  144. 'updateAvailable': updateAvailable,
  145. 'deemixVersion': deemixVersion}
  146. )
  147. if serverwide_arl:
  148. login(arl)
  149. else:
  150. emit('init_autologin')
  151. queue, queueComplete, queueList, currentItem = app.initDownloadQueue()
  152. if len(queueList.keys()):
  153. emit('init_downloadQueue',{
  154. 'queue': queue,
  155. 'queueComplete': queueComplete,
  156. 'queueList': queueList,
  157. 'currentItem': currentItem
  158. })
  159. emit('init_home', app.get_home(session['dz']))
  160. emit('init_charts', app.get_charts(session['dz']))
  161. if not is_deezer_available:
  162. emit('deezerNotAvailable')
  163. @socketio.on('get_home_data')
  164. def get_home_data():
  165. emit('init_home', app.get_home(session['dz']))
  166. @socketio.on('get_charts_data')
  167. def get_charts_data():
  168. emit('init_charts', app.get_charts(session['dz']))
  169. @socketio.on('login')
  170. def login(arl, force=False, child=0):
  171. global firstConnection, is_deezer_available
  172. if not is_deezer_available:
  173. emit('logged_in', {'status': -1, 'arl': arl, 'user': session['dz'].user})
  174. return
  175. if child == None:
  176. child = 0
  177. arl = arl.strip()
  178. emit('logging_in')
  179. if not session['dz'].logged_in:
  180. result = session['dz'].login_via_arl(arl, int(child))
  181. else:
  182. if force:
  183. session['dz'] = Deezer()
  184. result = session['dz'].login_via_arl(arl, int(child))
  185. if result == 1:
  186. result = 3
  187. else:
  188. result = 2
  189. emit('logged_in', {'status': result, 'arl': arl, 'user': session['dz'].user})
  190. if firstConnection and result in [1, 3]:
  191. firstConnection = False
  192. app.restoreDownloadQueue(session['dz'], socket_interface)
  193. if result != 0:
  194. emit('familyAccounts', session['dz'].childs)
  195. emit('init_favorites', app.getUserFavorites(session['dz']))
  196. @socketio.on('changeAccount')
  197. def changeAccount(child):
  198. emit('accountChanged', session['dz'].change_account(int(child)))
  199. emit('init_favorites', app.getUserFavorites(session['dz']))
  200. @socketio.on('logout')
  201. def logout():
  202. status = 0
  203. if session['dz'].logged_in:
  204. session['dz'] = Deezer()
  205. status = 0
  206. else:
  207. status = 1
  208. emit('logged_out', status)
  209. @socketio.on('mainSearch')
  210. def mainSearch(data):
  211. if data['term'].strip() != "":
  212. result = app.mainSearch(session['dz'], data['term'])
  213. result['ack'] = data.get('ack')
  214. emit('mainSearch', result)
  215. @socketio.on('search')
  216. def search(data):
  217. if data['term'].strip() != "":
  218. result = app.search(session['dz'], data['term'], data['type'], data['start'], data['nb'])
  219. result['type'] = data['type']
  220. result['ack'] = data.get('ack')
  221. emit('search', result)
  222. @socketio.on('queueRestored')
  223. def queueRestored():
  224. app.queueRestored(session['dz'], socket_interface)
  225. @socketio.on('addToQueue')
  226. def addToQueue(data):
  227. result = app.addToQueue(session['dz'], data['url'], data['bitrate'], interface=socket_interface, ack=data.get('ack'))
  228. if result == "Not logged in":
  229. emit('loginNeededToDownload')
  230. @socketio.on('removeFromQueue')
  231. def removeFromQueue(uuid):
  232. app.removeFromQueue(uuid, interface=socket_interface)
  233. @socketio.on('removeFinishedDownloads')
  234. def removeFinishedDownloads():
  235. app.removeFinishedDownloads(interface=socket_interface)
  236. @socketio.on('cancelAllDownloads')
  237. def cancelAllDownloads():
  238. app.cancelAllDownloads(interface=socket_interface)
  239. @socketio.on('saveSettings')
  240. def saveSettings(settings, spotifyCredentials, spotifyUser):
  241. app.saveSettings(settings, session['dz'])
  242. app.setSpotifyCredentials(spotifyCredentials)
  243. socketio.emit('updateSettings', (settings, spotifyCredentials))
  244. if spotifyUser != False:
  245. emit('updated_userSpotifyPlaylists', app.updateUserSpotifyPlaylists(spotifyUser))
  246. @socketio.on('getTracklist')
  247. def getTracklist(data):
  248. if data['type'] == 'artist':
  249. artistAPI = session['dz'].get_artist(data['id'])
  250. artistAPI['releases'] = session['dz'].get_artist_discography_gw(data['id'], 100)
  251. emit('show_artist', artistAPI)
  252. elif data['type'] == 'spotifyplaylist':
  253. playlistAPI = app.getSpotifyPlaylistTracklist(data['id'])
  254. for i in range(len(playlistAPI['tracks'])):
  255. playlistAPI['tracks'][i] = playlistAPI['tracks'][i]['track']
  256. playlistAPI['tracks'][i]['selected'] = False
  257. emit('show_spotifyplaylist', playlistAPI)
  258. else:
  259. releaseAPI = getattr(session['dz'], 'get_' + data['type'])(data['id'])
  260. releaseTracksAPI = getattr(session['dz'], 'get_' + data['type'] + '_tracks')(data['id'])['data']
  261. tracks = []
  262. showdiscs = False
  263. if data['type'] == 'album' and len(releaseTracksAPI) and releaseTracksAPI[-1]['disk_number'] != 1:
  264. current_disk = 0
  265. showdiscs = True
  266. for track in releaseTracksAPI:
  267. if showdiscs and int(track['disk_number']) != current_disk:
  268. current_disk = int(track['disk_number'])
  269. tracks.append({'type': 'disc_separator', 'number': current_disk})
  270. track['selected'] = False
  271. tracks.append(track)
  272. releaseAPI['tracks'] = tracks
  273. emit('show_' + data['type'], releaseAPI)
  274. @socketio.on('analyzeLink')
  275. def analyzeLink(link):
  276. if 'deezer.page.link' in link:
  277. link = urlopen(link).url
  278. (type, data) = app.analyzeLink(session['dz'], link)
  279. if len(data):
  280. emit('analyze_'+type, data)
  281. else:
  282. emit('analyze_notSupported')
  283. @socketio.on('getChartTracks')
  284. def getChartTracks(id):
  285. emit('setChartTracks', session['dz'].get_playlist_tracks(id)['data'])
  286. @socketio.on('update_userFavorites')
  287. def update_userFavorites():
  288. emit('updated_userFavorites', app.getUserFavorites(session['dz']))
  289. @socketio.on('update_userSpotifyPlaylists')
  290. def update_userSpotifyPlaylists(spotifyUser):
  291. if spotifyUser != False:
  292. emit('updated_userSpotifyPlaylists', app.updateUserSpotifyPlaylists(spotifyUser))
  293. @socketio.on('update_userPlaylists')
  294. def update_userPlaylists():
  295. emit('updated_userPlaylists', app.updateUserPlaylists(session['dz']))
  296. @socketio.on('update_userAlbums')
  297. def update_userAlbums():
  298. emit('updated_userAlbums', app.updateUserAlbums(session['dz']))
  299. @socketio.on('update_userArtists')
  300. def update_userArtists():
  301. emit('updated_userArtists', app.updateUserArtists(session['dz']))
  302. @socketio.on('update_userTracks')
  303. def update_userTracks():
  304. emit('updated_userTracks', app.updateUserTracks(session['dz']))
  305. @socketio.on('openDownloadsFolder')
  306. def openDownloadsFolder():
  307. folder = app.getDownloadFolder()
  308. if sys.platform == 'darwin':
  309. subprocess.check_call(['open', folder])
  310. elif sys.platform == 'linux':
  311. subprocess.check_call(['xdg-open', folder])
  312. elif sys.platform == 'win32':
  313. subprocess.check_call(['explorer', folder])
  314. @socketio.on('selectDownloadFolder')
  315. def selectDownloadFolder():
  316. if gui:
  317. gui.selectDownloadFolder_trigger.emit()
  318. gui._selectDownloadFolder_semaphore.acquire()
  319. result = gui.downloadFolder
  320. if result:
  321. emit('downloadFolderSelected', result)
  322. else:
  323. print("Can't open folder selection, you're not running the gui")
  324. @socketio.on('applogin')
  325. def applogin():
  326. if gui:
  327. if not session['dz'].logged_in:
  328. gui.appLogin_trigger.emit()
  329. gui._appLogin_semaphore.acquire()
  330. if gui.arl:
  331. emit('applogin_arl', gui.arl)
  332. gui.arl = None
  333. else:
  334. emit('logged_in', {'status': 2, 'user': session['dz'].user})
  335. else:
  336. print("Can't open login page, you're not running the gui")
  337. def run_server(port, host="127.0.0.1", portable=None, mainWindow=None):
  338. global app, gui, arl, is_deezer_available
  339. app = deemix(portable)
  340. gui = mainWindow
  341. if serverwide_arl:
  342. arl = app.getConfigArl()
  343. check_for_updates()
  344. if not check_deezer_availability():
  345. is_deezer_available = False
  346. print("Deezer is not available in your country, you should use a VPN to use this app.")
  347. print("Starting server at http://" + host + ":" + str(port))
  348. socketio.run(server, host=host, port=port)
  349. if __name__ == '__main__':
  350. port = 6595
  351. host = "127.0.0.1"
  352. portable = None
  353. if len(sys.argv) >= 2:
  354. try:
  355. port = int(sys.argv[1])
  356. except ValueError:
  357. pass
  358. if '--portable' in sys.argv:
  359. portable = path.join(path.dirname(path.realpath(__file__)), 'config')
  360. if '--host' in sys.argv:
  361. host = str(sys.argv[sys.argv.index("--host")+1])
  362. signal.signal(signal.SIGINT, shutdown_handler)
  363. signal.signal(signal.SIGTERM, shutdown_handler)
  364. run_server(port, host, portable)