diff --git a/src/application.py b/src/application.py index ad8bb46..b51a79c 100644 --- a/src/application.py +++ b/src/application.py @@ -6,7 +6,7 @@ authorEmail = "manuel@manuelcortez.net" copyright = "Copyright (C) 2018, Manuel Cortez" description = name+" Is an application that will allow you to download music from popular sites such as youtube, zaycev.net." url = "https://manuelcortez.net/music_dl" -#update_url = "https://raw.githubusercontent.com/manuelcortez/socializer/master/update-files/socializer.json" +update_url = "https://manuelcortez.net/music_dl/update" # The short name will be used for detecting translation files. See languageHandler for more details. short_name = "musicdl" translators = [] \ No newline at end of file diff --git a/src/controller/mainController.py b/src/controller/mainController.py index c6192e1..129fadb 100644 --- a/src/controller/mainController.py +++ b/src/controller/mainController.py @@ -7,6 +7,7 @@ import utils from pubsub import pub from wxUI import mainWindow, menus from extractors import zaycev, youtube +from update import updater from . import player log = logging.getLogger("controller.main") @@ -18,7 +19,6 @@ class Controller(object): log.debug("Starting main controller...") # Setting up the player object player.setup() - # Instantiate the only available extractor for now. # Get main window self.window = mainWindow.mainWindow() log.debug("Main window created") @@ -31,6 +31,7 @@ class Controller(object): self.timer.Start(75) self.window.vol_slider.SetValue(player.player.volume) # Shows window. + utils.call_threaded(updater.do_update) self.window.Show() def get_status_info(self): diff --git a/src/update/__init__.py b/src/update/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/update/update.py b/src/update/update.py new file mode 100644 index 0000000..84299dc --- /dev/null +++ b/src/update/update.py @@ -0,0 +1,116 @@ +from logging import getLogger +logger = getLogger('update') + +import contextlib +import io +import os +import platform +import requests +import tempfile +try: + import czipfile as zipfile +except ImportError: + import zipfile + +from platform_utils import paths + +def perform_update(endpoint, current_version, app_name='', password=None, update_available_callback=None, progress_callback=None, update_complete_callback=None): + requests_session = create_requests_session(app_name=app_name, version=current_version) + available_update = find_update(endpoint, requests_session=requests_session) + if not available_update: + logger.debug("No update available") + return False + available_version = float(available_update['current_version']) + if not float(available_version) > float(current_version) or platform.system()+platform.architecture()[0][:2] not in available_update['downloads']: + logger.debug("No update for this architecture") + return False + available_description = available_update.get('description', None) + update_url = available_update ['downloads'][platform.system()+platform.architecture()[0][:2]] + logger.info("A new update is available. Version %s" % available_version) + if callable(update_available_callback) and not update_available_callback(version=available_version, description=available_description): #update_available_callback should return a falsy value to stop the process + logger.info("User canceled update.") + return + base_path = tempfile.mkdtemp() + download_path = os.path.join(base_path, 'update.zip') + update_path = os.path.join(base_path, 'update') + downloaded = download_update(update_url, download_path, requests_session=requests_session, progress_callback=progress_callback) + extracted = extract_update(downloaded, update_path, password=password) + bootstrap_path = move_bootstrap(extracted) + execute_bootstrap(bootstrap_path, extracted) + logger.info("Update prepared for installation.") + if callable(update_complete_callback): + update_complete_callback() + +def create_requests_session(app_name=None, version=None): + user_agent = '' + session = requests.session() + if app_name: + user_agent = ' %s/%r' % (app_name, version) + session.headers['User-Agent'] = session.headers['User-Agent'] + user_agent + return session + +def find_update(endpoint, requests_session): + response = requests_session.get(endpoint) + response.raise_for_status() + content = response.json() + return content + +def download_update(update_url, update_destination, requests_session, progress_callback=None, chunk_size=io.DEFAULT_BUFFER_SIZE): + total_downloaded = total_size = 0 + with io.open(update_destination, 'w+b') as outfile: + download = requests_session.get(update_url, stream=True) + total_size = int(download.headers.get('content-length', 0)) + logger.debug("Total update size: %d" % total_size) + download.raise_for_status() + for chunk in download.iter_content(chunk_size): + outfile.write(chunk) + total_downloaded += len(chunk) + if callable(progress_callback): + call_callback(progress_callback, total_downloaded, total_size) + logger.debug("Update downloaded") + return update_destination + +def extract_update(update_archive, destination, password=None): + """Given an update archive, extracts it. Returns the directory to which it has been extracted""" + with contextlib.closing(zipfile.ZipFile(update_archive)) as archive: + if password: + archive.setpassword(password) + archive.extractall(path=destination) + logger.debug("Update extracted") + return destination + +def move_bootstrap(extracted_path): + working_path = os.path.abspath(os.path.join(extracted_path, '..')) + if platform.system() == 'Darwin': + extracted_path = os.path.join(extracted_path, 'Contents', 'Resources') + downloaded_bootstrap = os.path.join(extracted_path, bootstrap_name()) + new_bootstrap_path = os.path.join(working_path, bootstrap_name()) + os.rename(downloaded_bootstrap, new_bootstrap_path) + return new_bootstrap_path + +def execute_bootstrap(bootstrap_path, source_path): + arguments = r'"%s" "%s" "%s" "%s"' % (os.getpid(), source_path, paths.app_path(), paths.get_executable()) + if platform.system() == 'Windows': + import win32api + win32api.ShellExecute(0, 'open', bootstrap_path, arguments, '', 5) + else: + import subprocess + make_executable(bootstrap_path) + subprocess.Popen(['%s %s' % (bootstrap_path, arguments)], shell=True) + logger.info("Bootstrap executed") + +def bootstrap_name(): + if platform.system() == 'Windows': return 'bootstrap.exe' + if platform.system() == 'Darwin': return 'bootstrap-mac.sh' + return 'bootstrap-lin.sh' + +def make_executable(path): + import stat + st = os.stat(path) + os.chmod(path, st.st_mode | stat.S_IEXEC) + +def call_callback(callback, *args, **kwargs): +# try: + callback(*args, **kwargs) +# except: +# logger.exception("Failed calling callback %r with args %r and kwargs %r" % (callback, args, kwargs)) diff --git a/src/update/updater.py b/src/update/updater.py new file mode 100644 index 0000000..1627d21 --- /dev/null +++ b/src/update/updater.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +import application +import platform +import logging +from requests.exceptions import ConnectionError +from .import update +from .wxUpdater import * +logger = logging.getLogger("updater") + +def do_update(endpoint=application.update_url): + try: + return update.perform_update(endpoint=endpoint, current_version=application.version, app_name=application.name, update_available_callback=available_update_dialog, progress_callback=progress_callback, update_complete_callback=update_finished) + except ConnectionError: + logger.exception("Update failed.") \ No newline at end of file diff --git a/src/update/utils.py b/src/update/utils.py new file mode 100644 index 0000000..d681107 --- /dev/null +++ b/src/update/utils.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- + +def convert_bytes(n): + K, M, G, T, P = 1 << 10, 1 << 20, 1 << 30, 1 << 40, 1 << 50 + if n >= P: + return '%.2fPb' % (float(n) / T) + elif n >= T: + return '%.2fTb' % (float(n) / T) + elif n >= G: + return '%.2fGb' % (float(n) / G) + elif n >= M: + return '%.2fMb' % (float(n) / M) + elif n >= K: + return '%.2fKb' % (float(n) / K) + else: + return '%d' % n + +def seconds_to_string(seconds, precision=0): + day = seconds // 86400 + hour = seconds // 3600 + min = (seconds // 60) % 60 + sec = seconds - (hour * 3600) - (min * 60) + sec_spec = "." + str(precision) + "f" + sec_string = sec.__format__(sec_spec) + string = "" + if day == 1: + string += _(u"%d day, ") % day + elif day >= 2: + string += _(u"%d days, ") % day + if (hour == 1): + string += _(u"%d hour, ") % hour + elif (hour >= 2): + string += _("%d hours, ") % hour + if (min == 1): + string += _(u"%d minute, ") % min + elif (min >= 2): + string += _(u"%d minutes, ") % min + if sec >= 0 and sec <= 2: + string += _(u"%s second") % sec_string + else: + string += _(u"%s seconds") % sec_string + return string \ No newline at end of file diff --git a/src/update/wxUpdater.py b/src/update/wxUpdater.py new file mode 100644 index 0000000..1e3f809 --- /dev/null +++ b/src/update/wxUpdater.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +import wx +import application +import utils + +progress_dialog = None + +def available_update_dialog(version, description): + dialog = wx.MessageDialog(None, _(u"There's a new %s version available. Would you like to download it now?\n\n %s version: %s\n\nChanges:\n%s") % (application.name, application.name, version, description), _(u"New version for %s") % application.name, style=wx.YES|wx.NO|wx.ICON_WARNING) + if dialog.ShowModal() == wx.ID_YES: + return True + else: + return False + +def create_progress_dialog(): + return wx.ProgressDialog(_(u"Download in Progress"), _(u"Downloading the new version..."), parent=None, maximum=100) + +def progress_callback(total_downloaded, total_size): + global progress_dialog + if progress_dialog == None: + progress_dialog = create_progress_dialog() + progress_dialog.Show() + if total_downloaded == total_size: + progress_dialog.Destroy() + else: + progress_dialog.Update((total_downloaded*100)/total_size, _(u"Updating... %s of %s") % (str(utils.convert_bytes(total_downloaded)), str(utils.convert_bytes(total_size)))) + +def update_finished(): + ms = wx.MessageDialog(None, _(u"The update has been downloaded and installed successfully. Press OK to continue."), _(u"Done!")).ShowModal() \ No newline at end of file diff --git a/src/widgetUtils/__init__.pyc b/src/widgetUtils/__init__.pyc deleted file mode 100644 index 460139a..0000000 Binary files a/src/widgetUtils/__init__.pyc and /dev/null differ diff --git a/src/widgetUtils/wxUtils.pyc b/src/widgetUtils/wxUtils.pyc deleted file mode 100644 index 80fdee1..0000000 Binary files a/src/widgetUtils/wxUtils.pyc and /dev/null differ