checkpoint. continued work on downloader
This commit is contained in:
@@ -1,16 +1,29 @@
|
|||||||
from pathlib import Path
|
import platform
|
||||||
|
import re
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from queue import Queue, Empty
|
from collections import deque, namedtuple
|
||||||
|
from pathlib import Path
|
||||||
|
from queue import Empty, Queue
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from parsel import Selector
|
||||||
from PyQt5 import QtCore, QtGui, QtNetwork, QtWebEngineWidgets, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtNetwork, QtWebEngineWidgets, QtWidgets
|
||||||
from PyQt5.uic import loadUi
|
from PyQt5.uic import loadUi
|
||||||
|
|
||||||
|
try:
|
||||||
|
import win32api
|
||||||
|
except ImportError:
|
||||||
|
if platform.system()=='Windows':
|
||||||
|
raise #require win32api on windows
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Downloader(QtWidgets.QMainWindow):
|
class Downloader(QtWidgets.QMainWindow):
|
||||||
|
|
||||||
download_progress = QtCore.pyqtSignal(str, int, int)
|
download_progress = QtCore.pyqtSignal(str, int, int)
|
||||||
|
LoadedPage = namedtuple("LoadedPage", ("success", "html"))
|
||||||
|
|
||||||
def __init__(self, download_queue, install_ready, download_directory, parent = None, flags = None):
|
def __init__(self, download_queue, install_ready, download_errors, download_directory, parent = None, flags = None):
|
||||||
super().__init__(parent, flags)
|
super().__init__(parent, flags)
|
||||||
loadUi(Path(__file__).parent / "downloader.ui", baseinstance = self)
|
loadUi(Path(__file__).parent / "downloader.ui", baseinstance = self)
|
||||||
self.hide()
|
self.hide()
|
||||||
@@ -36,6 +49,7 @@ class Downloader(QtWidgets.QMainWindow):
|
|||||||
#set attributes
|
#set attributes
|
||||||
self._download_queue = download_queue
|
self._download_queue = download_queue
|
||||||
self._install_ready = install_ready
|
self._install_ready = install_ready
|
||||||
|
self._download_errors = download_errors
|
||||||
self._download_directory = download_directory
|
self._download_directory = download_directory
|
||||||
|
|
||||||
|
|
||||||
@@ -61,6 +75,8 @@ class Downloader(QtWidgets.QMainWindow):
|
|||||||
self._check_history()
|
self._check_history()
|
||||||
|
|
||||||
def _check_history(self):
|
def _check_history(self):
|
||||||
|
"""Checks if the current web view has history to navigate forward or backward to. Disables or enables the forward/backward buttons accordingly
|
||||||
|
"""
|
||||||
if self.web_view.history().canGoBack():
|
if self.web_view.history().canGoBack():
|
||||||
self.back_button.setEnabled(True)
|
self.back_button.setEnabled(True)
|
||||||
else:
|
else:
|
||||||
@@ -72,12 +88,20 @@ class Downloader(QtWidgets.QMainWindow):
|
|||||||
self.forward_button.setEnabled(False)
|
self.forward_button.setEnabled(False)
|
||||||
|
|
||||||
def _cancel_manual_search(self):
|
def _cancel_manual_search(self):
|
||||||
|
"""Action handler for the cancel action. Puts the received tool info into errors queue and immediately exits
|
||||||
|
"""
|
||||||
|
self._download_errors.put(self._tool_info)
|
||||||
self.close()
|
self.close()
|
||||||
|
self.deleteLater()
|
||||||
QtCore.QThread.currentThread().exit(1)
|
QtCore.QThread.currentThread().exit(1)
|
||||||
|
|
||||||
def _cancel_manual_search_open_issue(self):
|
def _cancel_manual_search_open_issue(self):
|
||||||
self.close()
|
"""Action handler for the cancel and open a bug report. Same as cancel, except it will open the github issues page in the default browser first
|
||||||
|
"""
|
||||||
|
self._download_errors.put(self._tool_info)
|
||||||
webbrowser.open_new_tab("https://github.com/norweeg/portable-computing-toolkit-installer/issues/new")
|
webbrowser.open_new_tab("https://github.com/norweeg/portable-computing-toolkit-installer/issues/new")
|
||||||
|
self.close()
|
||||||
|
self.deleteLater()
|
||||||
QtCore.QThread.currentThread().exit(1)
|
QtCore.QThread.currentThread().exit(1)
|
||||||
|
|
||||||
def begin_manual_search(self):
|
def begin_manual_search(self):
|
||||||
@@ -87,32 +111,128 @@ class Downloader(QtWidgets.QMainWindow):
|
|||||||
self.web_view.load(QtCore.QUrl(self._home_page))
|
self.web_view.load(QtCore.QUrl(self._home_page))
|
||||||
|
|
||||||
def begin_auto_search(self):
|
def begin_auto_search(self):
|
||||||
|
"""Begins the search process by getting tool info from a download queue and finalizes some GUI elements (in case manual search is necessary)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
tool_info = self._download_queue.get(timeout = 1)
|
tool_info = self._download_queue.get(timeout = 1)
|
||||||
except Empty:
|
except Empty:
|
||||||
QtCore.QThread.currentThread().quit()
|
QtCore.QThread.currentThread().quit()
|
||||||
else:
|
else:
|
||||||
self._tool_name = tool_info["name"]
|
|
||||||
self._tool_info = tool_info
|
self._tool_info = tool_info
|
||||||
|
self._tool_name = tool_info["name"]
|
||||||
|
self._download_page_selector = deque(tool_info["search"]["selector"])
|
||||||
self._home_page = tool_info["homepage"]
|
self._home_page = tool_info["homepage"]
|
||||||
#name window from attribute
|
#name window from attribute
|
||||||
self.setWindowTitle(f"Find {self._tool_name}")
|
self.setWindowTitle(f"Find {self._tool_name}")
|
||||||
#get homepage from attributes and set home button to load it
|
#get homepage from attributes and set home button to load it
|
||||||
self.home_button.clicked.connect(lambda: self.web_view.load(QtCore.QUrl(self._home_page)))
|
self.home_button.clicked.connect(lambda: self.web_view.load(QtCore.QUrl(self._home_page)))
|
||||||
self.web_view.loadFinished.connect()
|
self.web_view.loadFinished.connect(lambda status: self._load_result.put(Downloader.LoadedPage(status, self.web_view.page().toHtml())))
|
||||||
|
try:
|
||||||
|
self._download_file(self._find_installer_url(self._tool_info["page"]))
|
||||||
|
except:
|
||||||
|
self.begin_manual_search()
|
||||||
|
|
||||||
self._download_file(self._find_installer_url())
|
def _find_installer_url(self, url):
|
||||||
|
"""Finds the URL of the next page to navigate to or the item to download using the search selectors from a tool info dictionary. The fully-rendered
|
||||||
|
HTML will be scraped, in case the desired URL is rendered by JavaScript
|
||||||
|
|
||||||
def _find_installer_url(self):
|
Args:
|
||||||
self.web_view.load(QtCore.QUrl(self._tool_info["page"]))
|
url (str): The URL of the page to be scraped
|
||||||
|
"""
|
||||||
|
#load the url with a browser that will render page, including urls generated by JavaScript and get result
|
||||||
|
self.web_view.load(QtCore.QUrl(url))
|
||||||
load_result = self._load_result.get()
|
load_result = self._load_result.get()
|
||||||
|
#if page loaded successfully, parse the results, otherwise return nothing
|
||||||
|
if load_result.success:
|
||||||
|
page = Selector(text = load_result.html)
|
||||||
|
self._load_result.task_done()
|
||||||
|
else:
|
||||||
|
self._load_result.task_done()
|
||||||
|
return
|
||||||
|
'''if tool has a download page selector, we need to follow one or more links from the "page" url to find the download page to search
|
||||||
|
portableapps.com loves to make you do this'''
|
||||||
|
if self._download_page_selector:
|
||||||
|
next_page_query = self._download_page_selector.popleft()
|
||||||
|
return self._find_installer_url(urljoin(url, page.css(next_page_query).attrib["href"]))
|
||||||
|
#get all links in the current page and search for the tool's installer file using a regular expression
|
||||||
|
for link in [urljoin(url, anchor.attrib["href"]) for anchor in page.xpath("//a")]:
|
||||||
|
if re.search(self._tool_info["search"]["filename regex"], link):
|
||||||
|
#the link matched the regular expression, our download URL is found!
|
||||||
|
return link
|
||||||
|
#if nothing was found by now, returns nothing. automated search has failed
|
||||||
|
return
|
||||||
|
|
||||||
def _download_file(self, url):
|
def _download_file(self, url):
|
||||||
pass
|
"""Initiates a download of url
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): The URL of the file to be downloaded
|
||||||
|
"""
|
||||||
|
#no url means autosearch has failed
|
||||||
|
assert url
|
||||||
|
try:
|
||||||
|
self.web_view.loadFinished.disconnect()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.web_view.page().profile().downloadRequested.connect(self._process_download)
|
||||||
|
self.web_view.load(url)
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot("QWebEngineDownloadItem*")
|
||||||
|
def _process_download(self, download_item):
|
||||||
|
self._tool_info["download url"] = download_item.url().toString()
|
||||||
|
self._tool_info["mimetype"] = download_item.mimeType()
|
||||||
|
suggested_path = Path(download_item.path())
|
||||||
|
filename = suggested_path.name
|
||||||
|
download_item.setPath(str(Path(self._download_directory/filename)))
|
||||||
|
self._tool_info["filename"] = Path(download_item.path())
|
||||||
|
download_item.downloadProgress.connect(lambda x, y: self.download_progress.emit(self._tool_name, x, y))
|
||||||
|
download_item.stateChanged.connect(self._download_interrupted)
|
||||||
|
download_item.finished(self._check_finished)
|
||||||
|
download_item.accept()
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot("QWebEngineDownloadItem::DownloadState")
|
||||||
|
def _download_interrupted(self, state):
|
||||||
|
"""Receives the QWebEngineDownloadItem's stateChanged signal and, if the state is "interrupted", trigger a failure
|
||||||
|
"""
|
||||||
|
if state == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadInterrupted:
|
||||||
|
sender = self.sender()
|
||||||
|
self._tool_info["error reason"] = sender.interruptReasonString()
|
||||||
|
sender.cancel()
|
||||||
|
self._tool_info["filename"].unlink()
|
||||||
|
self._tool_info.pop("filename", None)
|
||||||
|
self._download_errors.put(self._tool_info)
|
||||||
|
self.close()
|
||||||
|
self.deleteLater()
|
||||||
|
QtCore.QThread.currentThread().exit(1)
|
||||||
|
|
||||||
|
@QtCore.pyqtSlot()
|
||||||
|
def _check_finished(self):
|
||||||
|
"""Receives the QWebEngineDownloadItem's finished signal, verifies success, unblocks the file an triggers a success
|
||||||
|
"""
|
||||||
|
#get the downloaditem that sent this signal
|
||||||
|
sender = self.sender()
|
||||||
|
#if the download item is completed, unblock the file on windows, then close and exit this thread
|
||||||
|
if sender.state() == QtWebEngineWidgets.QWebEngineDownloadItem.DownloadCompleted:
|
||||||
|
if platform.system() == 'Windows':
|
||||||
|
self._unblock_file()
|
||||||
|
#check if the total bytes in the download is known. -1 means not known.
|
||||||
|
if sender.totalBytes() == -1:
|
||||||
|
#if not known, signal that this download has completed, since the progress thusfar has been ignored
|
||||||
|
self.download_progress.emit(self._tool_name, 1, 1)
|
||||||
|
self._install_ready.put(self._tool_info)
|
||||||
|
self.close()
|
||||||
|
self.deleteLater()
|
||||||
|
QtCore.QThread.currentThread().quit()
|
||||||
|
|
||||||
|
def _unblock_file(self):
|
||||||
|
"""Removes the "downladed from internet" Zone Identifier. Windows will prevent executables that have this set from executing.
|
||||||
|
Contents of zip files inherit this from the .zip they come from
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
win32api.DeleteFile(str(self._tool_info["filename"])+r":Zone.Identifier")
|
||||||
|
except win32api.error:
|
||||||
|
#just ignore the error if the above Zone Identifier is not set
|
||||||
|
pass
|
||||||
|
|
||||||
class DownloadWorker(QtCore.QRunnable):
|
class DownloadWorker(QtCore.QRunnable):
|
||||||
def __init__(self, download_queue, install_ready, download_error, download_directory, wizard):
|
def __init__(self, download_queue, install_ready, download_error, download_directory, wizard):
|
||||||
@@ -125,4 +245,4 @@ class DownloadWorker(QtCore.QRunnable):
|
|||||||
def run(self):
|
def run(self):
|
||||||
downloader = Downloader(self.download_queue, self._install_ready, self._download_error, self._download_directory)
|
downloader = Downloader(self.download_queue, self._install_ready, self._download_error, self._download_directory)
|
||||||
downloader.download_progress.connect(self._wizard.track_progress)
|
downloader.download_progress.connect(self._wizard.track_progress)
|
||||||
downloader.begin_auto_search()
|
downloader.begin_auto_search()
|
||||||
|
|||||||
@@ -370,7 +370,10 @@ class InstallerWizard(QtWidgets.QWizard):
|
|||||||
if tool_name not in [tool["name"] for tool in self.__tools__]:
|
if tool_name not in [tool["name"] for tool in self.__tools__]:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self._download_progress[tool_name] = bytes_received/bytes_total
|
try:
|
||||||
|
self._download_progress[tool_name] = bytes_received/bytes_total
|
||||||
|
except ZeroDivisionError:
|
||||||
|
self._download_progress[tool_name] = 0
|
||||||
self._progress_update_lock.acquire()
|
self._progress_update_lock.acquire()
|
||||||
self.progress_bar.setValue(sum(self._download_progress.values()))
|
self.progress_bar.setValue(sum(self._download_progress.values()))
|
||||||
self._progress_update_lock.release()
|
self._progress_update_lock.release()
|
||||||
|
|||||||
Reference in New Issue
Block a user