def createRequest(self, op, request, device = None ):
		global block_list
		try:
			path = (request.url().toString())
		except UnicodeEncodeError:
			path = (request.url().path())
			
		lower_path = path.lower()
		block_list = [
		"doubleclick.net" ,"adnxs", r"||youtube-nocookie.com/gen_204?", 
		r"youtube.com###watch-branded-actions", "imagemapurl",
		"b.scorecardresearch.com","rightstuff.com","scarywater.net","popup.js",
		"banner.htm","_tribalfusion","||n4403ad.doubleclick.net^$third-party",
		".googlesyndication.com","graphics.js","fonts.googleapis.com/css",
		"s0.2mdn.net","server.cpmstar.com","||banzai/banner.$subdocument",
		"@@||anime-source.com^$document","/pagead2.","frugal.gif",
		"jriver_banner.png","show_ads.js",
		'##a[href^="http://billing.frugalusenet.com/"]',
		"http://jriver.com/video.html","||animenewsnetwork.com^*.aframe?",
		"||contextweb.com^$third-party",".gutter",".iab",
		'http://www.animenewsnetwork.com/assets/[^"]*.jpg','revcontent'
		]
		block = False
		for l in block_list:
			if l in lower_path:
				block = True
				break
		if block:
			return QNetworkAccessManager.createRequest(self, QNetworkAccessManager.GetOperation, QtNetwork.QNetworkRequest(QtCore.QUrl()))
		else:
			return QNetworkAccessManager.createRequest(self, op, request, device)
Exemple #2
0
def qnam(qapp):
    """Session-wide QNetworkAccessManager."""
    from PyQt5.QtNetwork import QNetworkAccessManager

    nam = QNetworkAccessManager()
    nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
    return nam
	def createRequest(self, op, request, device = None ):
		global block_list,TMP_DIR
		try:
			urlLnk = (request.url()).toString()
			path = (request.url().toString())
		except UnicodeEncodeError:
			path = (request.url().path())
		
		if '9anime.to' in path and 'episode/info?' in path:
			f = open(os.path.join(TMP_DIR,'lnk.txt'),'w')
			f.write(path)
			f.close()
		lower_path = path.lower()
		#block_list = []
		block_list = ["doubleclick.net" ,"ads",'.gif','.css','facebook','.aspx', r"||youtube-nocookie.com/gen_204?", r"youtube.com###watch-branded-actions", "imagemapurl","b.scorecardresearch.com","rightstuff.com","scarywater.net","popup.js","banner.htm","_tribalfusion","||n4403ad.doubleclick.net^$third-party",".googlesyndication.com","graphics.js","fonts.googleapis.com/css","s0.2mdn.net","server.cpmstar.com","||banzai/banner.$subdocument","@@||anime-source.com^$document","/pagead2.","frugal.gif","jriver_banner.png","show_ads.js",'##a[href^="http://billing.frugalusenet.com/"]',"http://jriver.com/video.html","||animenewsnetwork.com^*.aframe?","||contextweb.com^$third-party",".gutter",".iab",'http://www.animenewsnetwork.com/assets/[^"]*.jpg','revcontent']
		block = False
		for l in block_list:
			if l in lower_path:
				block = True
				break
		if block:
			return QNetworkAccessManager.createRequest(self, QNetworkAccessManager.GetOperation, QtNetwork.QNetworkRequest(QtCore.QUrl()))
		else:
			if 'itag=' in urlLnk and 'redirector' not in urlLnk:
				print('*********')
				#f = open(os.path.join(TMP_DIR,'lnk.txt'),'w')
				#f.write(urlLnk)
				#f.close()
				return QNetworkAccessManager.createRequest(self, op, request, device)
			
			else:
				return QNetworkAccessManager.createRequest(self, op, request, device)
    def __init__(self):
        QNetworkAccessManager.__init__(self)
        url = QUrl("http://localhost:8085/events")
        req = QNetworkRequest(url)
        self.reply = self.get(req)

        self.reply.readyRead.connect(self.on_read_data)
        self.reply.finished.connect(self.on_finished)
        self.reply.error.connect(self.on_error)
 def __init__(self):
     QNetworkAccessManager.__init__(self)
     self.dispatch_map = {
         'GET': self.perform_get,
         'PATCH': self.perform_patch,
         'PUT': self.perform_put,
         'DELETE': self.perform_delete,
         'POST': self.perform_post
     }
     self.protocol = DEFAULT_API_PROTOCOL
     self.host = DEFAULT_API_HOST
     self.port = DEFAULT_API_PORT
 def __init__(self, api_port):
     QNetworkAccessManager.__init__(self)
     url = QUrl("http://localhost:%d/events" % api_port)
     self.request = QNetworkRequest(url)
     self.failed_attempts = 0
     self.connect_timer = QTimer()
     self.current_event_string = ""
     self.tribler_version = "Unknown"
     self.reply = None
     self.emitted_tribler_started = False  # We should only emit tribler_started once
     self.shutting_down = False
     self._logger = logging.getLogger('TriblerGUI')
 def createRequest(self, operation, request, data):
     try:
         requestFunc = self._dispatcher[request.url().scheme()]
     except KeyError:
         self.addHeadersToRequest(request)
         return QNetworkAccessManager.createRequest(self, operation, request, data)
     return requestFunc(self, operation, request, data)
Exemple #8
0
    def browsePackages(self) -> None:
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")

        # Gather installed packages:
        self._updateInstalledModels()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")

        if not self._dialog:
            Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.")
            return

        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()
Exemple #9
0
	def __init__(self, **kwargs):
		super(Ycm, self).__init__(**kwargs)

		self.addr = None
		"""Address of the ycmd server."""

		self.port = 0
		"""TCP port of the ycmd server."""

		self._ready = False
		self.secret = ''
		self.config = {}

		self.proc = QProcess()
		self.proc.started.connect(self.procStarted)
		self.proc.errorOccurred.connect(self.procError)
		self.proc.finished.connect(self.procFinished)

		self.pingTimer = QTimer(self)
		self.pingTimer.timeout.connect(self.ping)

		self.network = QNetworkAccessManager()

		qApp().aboutToQuit.connect(self.stop)

		self.addCategory('ycm_control')
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference("um3networkprinting/manual_instances", "") #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
Exemple #11
0
    def createRequest(self, operation, request, device):
        path = o = request.url().toString()

        if path.startswith('app://') or path.startswith('lens://'):
            if path == 'app:///':
                path = 'file://' + self._uri_app_base + 'app.html'
                logger.debug('Loading app resource: {0} ({1})'.format(o, path))

            elif path.startswith('app://'):
                path = path.replace('app://', 'file://' + self._uri_app_base)
                logger.debug('Loading app resource: {0} ({1})'.format(o, path))

                # variable substitution
                path = path.replace('$backend', 'qt5')

            elif path.startswith('lens://'):
                path = path.replace('lens://', 'file://' + self._uri_lens_base)
                logger.debug('Loading lens resource: {0} ({1})'.format(o, path))

                # make lens.css backend specific
                path = path.replace('lens.css', 'lens-qt5.css')

            request.setUrl(QUrl(QString(path)))

        return QNetworkAccessManager.createRequest(self, operation, request, device)
Exemple #12
0
    def createRequest(self, operation, request, device):
        path = o = request.url().toString()

        if path.startswith("app://") or path.startswith("lens://"):
            if path == "app:///":
                path = "file://" + self._uri_app_base + "app.html"
                logger.debug("Loading app resource: {0} ({1})".format(o, path))

            elif path.startswith("app://"):
                path = path.replace("app://", "file://" + self._uri_app_base)
                logger.debug("Loading app resource: {0} ({1})".format(o, path))

                # variable substitution
                path = path.replace("$backend", "qt5")

            elif path.startswith("lens://"):
                path = path.replace("lens://", "file://" + self._uri_lens_base)
                logger.debug("Loading lens resource: {0} ({1})".format(o, path))

                # make lens.css backend specific
                path = path.replace("lens.css", "lens-qt5.css")

            request.setUrl(QUrl(QString(path)))

        return QNetworkAccessManager.createRequest(self, operation, request, device)
    def __init__(self, parent=None):
        super(WeatherController, self).__init__(parent)

        self._dataReceived = False
        self._forecastDataReceived = False

        self._weather_data = CurrentWeatherData()
        self._weather_forecast_data = []
        self._data_model = ForecastDataModel()

        self._network_manager = QNetworkAccessManager(self)
        self._timer = QTimer(self)

        self._timer.timeout.connect(self.update_weather)

        self._current_weather = None
        self._forecast_weather = None
        self._api_key = ''

        self._last_update_time = ''

        self._requested_location = 'Dachau'

        try:
            with open('resources/api.txt') as f:
                self._api_key = f.readline()
        except FileNotFoundError:
            print('The api key is not found')
 def __init__(self, parent=None):
     """Init class."""
     super(Downloader, self).__init__(parent)
     self.setWindowTitle(__doc__)
     if not os.path.isfile(__file__) or not __source__:
         self.close()
     self._time, self._date = time.time(), datetime.now().isoformat()[:-7]
     self._url, self._dst = __source__, __file__
     log.debug("Downloading from {} to {}.".format(self._url, self._dst))
     if not self._url.lower().startswith("https:"):
         log.warning("Unsecure Download over plain text without SSL.")
     self.template = """<h3>Downloading</h3><hr><table>
     <tr><td><b>From:</b></td>      <td>{}</td>
     <tr><td><b>To:  </b></td>      <td>{}</td> <tr>
     <tr><td><b>Started:</b></td>   <td>{}</td>
     <tr><td><b>Actual:</b></td>    <td>{}</td> <tr>
     <tr><td><b>Elapsed:</b></td>   <td>{}</td>
     <tr><td><b>Remaining:</b></td> <td>{}</td> <tr>
     <tr><td><b>Received:</b></td>  <td>{} MegaBytes</td>
     <tr><td><b>Total:</b></td>     <td>{} MegaBytes</td> <tr>
     <tr><td><b>Speed:</b></td>     <td>{}</td>
     <tr><td><b>Percent:</b></td>     <td>{}%</td></table><hr>"""
     self.manager = QNetworkAccessManager(self)
     self.manager.finished.connect(self.save_downloaded_data)
     self.manager.sslErrors.connect(self.download_failed)
     self.progreso = self.manager.get(QNetworkRequest(QUrl(self._url)))
     self.progreso.downloadProgress.connect(self.update_download_progress)
     self.show()
     self.exec_()
Exemple #15
0
 def _create_request(self, operation, request, data):
     print(data.readAll())
     reply = QNetworkAccessManager.createRequest(self.network_manager,
                                                 operation,
                                                 request,
                                                 data)
     return reply
 def __init__(self, parent=None):
     """Init class."""
     super(Downloader, self).__init__(parent)
     self.setWindowTitle(__doc__)
     if not os.path.isfile(__file__) or not __source__:
         return
     if not os.access(__file__, os.W_OK):
         error_msg = ("Destination file permission denied (not Writable)! "
                      "Try again to Update but as root or administrator.")
         log.critical(error_msg)
         QMessageBox.warning(self, __doc__.title(), error_msg)
         return
     self._time, self._date = time.time(), datetime.now().isoformat()[:-7]
     self._url, self._dst = __source__, __file__
     log.debug("Downloading from {} to {}.".format(self._url, self._dst))
     if not self._url.lower().startswith("https:"):
         log.warning("Unsecure Download over plain text without SSL.")
     self.template = """<h3>Downloading</h3><hr><table>
     <tr><td><b>From:</b></td>      <td>{}</td>
     <tr><td><b>To:  </b></td>      <td>{}</td> <tr>
     <tr><td><b>Started:</b></td>   <td>{}</td>
     <tr><td><b>Actual:</b></td>    <td>{}</td> <tr>
     <tr><td><b>Elapsed:</b></td>   <td>{}</td>
     <tr><td><b>Remaining:</b></td> <td>{}</td> <tr>
     <tr><td><b>Received:</b></td>  <td>{} MegaBytes</td>
     <tr><td><b>Total:</b></td>     <td>{} MegaBytes</td> <tr>
     <tr><td><b>Speed:</b></td>     <td>{}</td>
     <tr><td><b>Percent:</b></td>     <td>{}%</td></table><hr>"""
     self.manager = QNetworkAccessManager(self)
     self.manager.finished.connect(self.save_downloaded_data)
     self.manager.sslErrors.connect(self.download_failed)
     self.progreso = self.manager.get(QNetworkRequest(QUrl(self._url)))
     self.progreso.downloadProgress.connect(self.update_download_progress)
     self.show()
     self.exec_()
class Fetch():
    data = QtCore.pyqtSignal(dict)

    def __init__(self, parent):
        self.session = QNetworkAccessManager(parent)
        self.cookies = QNetworkCookieJar()
        self.parent = parent
        self.session.setCookieJar(self.cookies)

    def base_handler(self, reply: QNetworkReply):
        try:
            response = json.loads(str(reply.readAll(), encoding='utf-8'))
            status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        except:
            self.parent.warn.add_warn("Http解析错误")
            return
        if reply.error() != QNetworkReply.NoError:
            self.handler_error(response, status_code)
        else:
            self.data.emit(response)

    def get(self, url, param=None):
        url = QtCore.QUrl(parse_url(url, param))
        request = QNetworkRequest(url)
        reply = self.session.get(request)
        return reply

    def post(self, url, param=None, data=None, json=True):
        if isinstance(data, dict):
            f = ''
            for i in data:
                f += '{}={}&'.format(i, data[i])
            data = f[:-1]
        byte_data = QtCore.QByteArray()
        byte_data.append(data)
        url = QtCore.QUrl(parse_url(url, param))
        request = QNetworkRequest(url)
        if json:
            request.setHeader(QNetworkRequest.ContentTypeHeader, 'application/json')
        reply = self.session.post(request, byte_data)
        return reply

    def handler_error(self, response, status_code):
        if isinstance(response, dict):
            message = response.get('error', 'unknown')
            self.parent.warn.add_warn('网络请求错误,错误码为{},原因为{}'.format(status_code, message))
Exemple #18
0
 def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None:
     super().__init__()
     self._manager = QNetworkAccessManager()
     self._account = account
     self._on_error = on_error
     self._upload = None  # type: Optional[ToolPathUploader]
     # In order to avoid garbage collection we keep the callbacks in this list.
     self._anti_gc_callbacks = []  # type: List[Callable[[], None]]
Exemple #19
0
 def post(self, url, data):
     if isinstance(data, dict): data = json.dumps(data)
     if isinstance(data, str): data = QByteArray(len(data), data)
     elif isinstance(data, bytes): data = QByteArray(data)
     else:
         data = str(data)
         data = QByteArray(len(data), data)
     return QNetworkAccessManager.post(self, self._getRequest(url), data)
Exemple #20
0
    def __pass(self, operation, request, data, info=None):
        """ short for "ignore filter, just generate the request" """

        if info is not None and info['notify']:
            show_labeled(info['message'], info['qurl'], color=info['color'])

        return QNetworkAccessManager.createRequest(
            self, operation, request, data)
Exemple #21
0
    def __init__(self, url):
        super(FLNetwork, self).__init__()
        self.url = url
        from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
        self.request = QNetworkRequest()

        self.manager = QNetworkAccessManager()
        # self.manager.readyRead.connect(self._slotNetworkStart)
        self.manager.finished['QNetworkReply*'].connect(self._slotNetworkFinished)
Exemple #22
0
 def _create_request(self, operation, request, data):
     # print(data)
     reply = QNetworkAccessManager.createRequest(self.conn,
                                                 operation,
                                                 request,
                                                 data)
     self.conn.new_reply = reply
     self.wv_reply = reply
     return reply
Exemple #23
0
 def __init__(self, parent):
     super().__init__(parent)
     self.set_url('http://google.ru')
     conn = QNetworkAccessManager()
     self.conn = conn
     self.r = QNetworkRequest()
     self.r.attribute(QNetworkRequest.CookieSaveControlAttribute, QVariant(True))
     # self.r.setHeader(QNetworkRequest.ContentTypeHeader, "application/x-www-form-urlencoded")
     # self.r.setRawHeader("Referer", "http://www.facebook.com/")
     # self.r.setRawHeader("Host", "www.facebook.com")
     self.cj = QNetworkCookieJar()
     conn.setCookieJar(self.cj)
     conn.createRequest = self._create_request
     self.wv = WebView()
     self.wv.show()
     self.wv.page().setNetworkAccessManager(conn)
     # self.wv.auth()
     self.loop = QEventLoop()
     pass
    def search_ticket(self, fromStation, toStation, date):
        try:
            self.netWorkManager=QNetworkAccessManager()
            self.reply=self.netWorkManager.get(self.req)
            self.reply.ignoreSslErrors()
            self.reply.finished.connect(self.search_finished)
            self.exec()

        except Exception as e:
            print("ip:"+self.domain+"查询发生错误:"+e.__str__())
            return False
Exemple #25
0
    def __block(self, message, info=None):
        """ short for "ignore request; generate default empty request" """

        if info is not None and info['notify']:
            show_labeled(info['message'], info['qurl'],
                         color=info['color'], detail=info['detail'])

        return QNetworkAccessManager.createRequest(
            self,
            QNetworkAccessManager.GetOperation,
            QNetworkRequest(QUrl(message)),
            None)
Exemple #26
0
    def start_load(self, url):
        '''Create a Poppler.Document from the given URL, QUrl or filename.
           Return, then asynchronously call self.load_cb.
        '''

        # If it's not a local file, we'll need to load it.
        # http://doc.qt.io/qt-5/qnetworkaccessmanager.html
        qurl = QUrl(url)
        if not qurl.scheme():
            qurl = QUrl.fromLocalFile(url)
        if not self.network_manager:
            self.network_manager = QNetworkAccessManager();
        self.network_manager.finished.connect(self.download_finished)
        self.network_manager.get(QNetworkRequest(qurl))
Exemple #27
0
    def start(self) -> None:
        self.stop()  # Ensure that previous requests (if any) are stopped.

        if not self._source_url:
            Logger.log("w", "Unable to start camera stream without target!")
            return
        self._started = True

        self._image_request = QNetworkRequest(self._source_url)
        if self._network_manager is None:
            self._network_manager = QNetworkAccessManager()

        self._image_reply = self._network_manager.get(self._image_request)
        self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
 def __init__(self, parent=None):
     super(TopSellingProducts, self).__init__(parent)
     
     self.ui = Ui_TopSellingProducts()
     self.ui.setupUi(self)
     self.ui.tableWidget.setColumnCount(5);
     self.ui.tableWidget.setColumnWidth(0, 120);
     self.ui.tableWidget.setColumnWidth(1, 480);
     
     self.manager = QNetworkAccessManager(self)
     self.manager.finished.connect(self.on_finished)
     self.manager.sslErrors.connect(self.on_sslErrors)
     
     self.replyMap = dict()
Exemple #29
0
    def __init__(self, parent=None):
        super(HttpWindow, self).__init__(parent)

        self.url = QUrl()
        self.qnam = QNetworkAccessManager()
        self.reply = None
        self.outFile = None
        self.httpGetId = 0
        self.httpRequestAborted = False

        self.urlLineEdit = QLineEdit('https://www.qt.io')

        urlLabel = QLabel("&URL:")
        urlLabel.setBuddy(self.urlLineEdit)
        self.statusLabel = QLabel(
                "Please enter the URL of a file you want to download.")
        self.statusLabel.setWordWrap(True)

        self.downloadButton = QPushButton("Download")
        self.downloadButton.setDefault(True)
        self.quitButton = QPushButton("Quit")
        self.quitButton.setAutoDefault(False)

        buttonBox = QDialogButtonBox()
        buttonBox.addButton(self.downloadButton, QDialogButtonBox.ActionRole)
        buttonBox.addButton(self.quitButton, QDialogButtonBox.RejectRole)

        self.progressDialog = QProgressDialog(self)

        self.urlLineEdit.textChanged.connect(self.enableDownloadButton)
        self.qnam.authenticationRequired.connect(
                self.slotAuthenticationRequired)
        self.qnam.sslErrors.connect(self.sslErrors)
        self.progressDialog.canceled.connect(self.cancelDownload)
        self.downloadButton.clicked.connect(self.downloadFile)
        self.quitButton.clicked.connect(self.close)

        topLayout = QHBoxLayout()
        topLayout.addWidget(urlLabel)
        topLayout.addWidget(self.urlLineEdit)

        mainLayout = QVBoxLayout()
        mainLayout.addLayout(topLayout)
        mainLayout.addWidget(self.statusLabel)
        mainLayout.addWidget(buttonBox)
        self.setLayout(mainLayout)

        self.setWindowTitle("HTTP")
        self.urlLineEdit.setFocus()
Exemple #30
0
class HttpWindow(QDialog):
    def __init__(self, parent=None):
        super(HttpWindow, self).__init__(parent)

        self.url = QUrl()
        self.qnam = QNetworkAccessManager()
        self.reply = None
        self.outFile = None
        self.httpGetId = 0
        self.httpRequestAborted = False

        self.urlLineEdit = QLineEdit('https://qt-project.org')

        urlLabel = QLabel("&URL:")
        urlLabel.setBuddy(self.urlLineEdit)
        self.statusLabel = QLabel(
                "Please enter the URL of a file you want to download.")
        self.statusLabel.setWordWrap(True)

        self.downloadButton = QPushButton("Download")
        self.downloadButton.setDefault(True)
        self.quitButton = QPushButton("Quit")
        self.quitButton.setAutoDefault(False)

        buttonBox = QDialogButtonBox()
        buttonBox.addButton(self.downloadButton, QDialogButtonBox.ActionRole)
        buttonBox.addButton(self.quitButton, QDialogButtonBox.RejectRole)

        self.progressDialog = QProgressDialog(self)

        self.urlLineEdit.textChanged.connect(self.enableDownloadButton)
        self.qnam.authenticationRequired.connect(
                self.slotAuthenticationRequired)
        self.qnam.sslErrors.connect(self.sslErrors)
        self.progressDialog.canceled.connect(self.cancelDownload)
        self.downloadButton.clicked.connect(self.downloadFile)
        self.quitButton.clicked.connect(self.close)

        topLayout = QHBoxLayout()
        topLayout.addWidget(urlLabel)
        topLayout.addWidget(self.urlLineEdit)

        mainLayout = QVBoxLayout()
        mainLayout.addLayout(topLayout)
        mainLayout.addWidget(self.statusLabel)
        mainLayout.addWidget(buttonBox)
        self.setLayout(mainLayout)

        self.setWindowTitle("HTTP")
        self.urlLineEdit.setFocus()

    def startRequest(self, url):
        self.reply = self.qnam.get(QNetworkRequest(url))
        self.reply.finished.connect(self.httpFinished)
        self.reply.readyRead.connect(self.httpReadyRead)
        self.reply.downloadProgress.connect(self.updateDataReadProgress)

    def downloadFile(self):
        self.url = QUrl(self.urlLineEdit.text())
        fileInfo = QFileInfo(self.url.path())
        fileName = fileInfo.fileName()

        if not fileName:
            fileName = 'index.html'

        if QFile.exists(fileName):
            ret = QMessageBox.question(self, "HTTP",
                    "There already exists a file called %s in the current "
                    "directory. Overwrite?" % fileName,
                    QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

            if ret == QMessageBox.No:
                return

            QFile.remove(fileName)

        self.outFile = QFile(fileName)
        if not self.outFile.open(QIODevice.WriteOnly):
            QMessageBox.information(self, "HTTP",
                    "Unable to save the file %s: %s." % (fileName, self.outFile.errorString()))
            self.outFile = None
            return

        self.progressDialog.setWindowTitle("HTTP")
        self.progressDialog.setLabelText("Downloading %s." % fileName)
        self.downloadButton.setEnabled(False)

        self.httpRequestAborted = False
        self.startRequest(self.url)

    def cancelDownload(self):
        self.statusLabel.setText("Download canceled.")
        self.httpRequestAborted = True
        self.reply.abort()
        self.downloadButton.setEnabled(True)

    def httpFinished(self):
        if self.httpRequestAborted:
            if self.outFile is not None:
                self.outFile.close()
                self.outFile.remove()
                self.outFile = None

            self.reply.deleteLater()
            self.reply = None
            self.progressDialog.hide()
            return

        self.progressDialog.hide()
        self.outFile.flush()
        self.outFile.close()

        redirectionTarget = self.reply.attribute(QNetworkRequest.RedirectionTargetAttribute)

        if self.reply.error():
            self.outFile.remove()
            QMessageBox.information(self, "HTTP",
                    "Download failed: %s." % self.reply.errorString())
            self.downloadButton.setEnabled(True)
        elif redirectionTarget is not None:
            newUrl = self.url.resolved(redirectionTarget.toUrl())

            ret = QMessageBox.question(self, "HTTP",
                    "Redirect to %s?" % newUrl.toString(),
                    QMessageBox.Yes | QMessageBox.No)

            if ret == QMessageBox.Yes:
                self.url = newUrl
                self.reply.deleteLater()
                self.reply = None
                self.outFile.open(QIODevice.WriteOnly)
                self.outFile.resize(0)
                self.startRequest(self.url)
                return
        else:
            fileName = QFileInfo(QUrl(self.urlLineEdit.text()).path()).fileName()
            self.statusLabel.setText("Downloaded %s to %s." % (fileName, QDir.currentPath()))

            self.downloadButton.setEnabled(True)

        self.reply.deleteLater()
        self.reply = None
        self.outFile = None

    def httpReadyRead(self):
        if self.outFile is not None:
            self.outFile.write(self.reply.readAll())

    def updateDataReadProgress(self, bytesRead, totalBytes):
        if self.httpRequestAborted:
            return

        self.progressDialog.setMaximum(totalBytes)
        self.progressDialog.setValue(bytesRead)

    def enableDownloadButton(self):
        self.downloadButton.setEnabled(self.urlLineEdit.text() != '')

    def slotAuthenticationRequired(self, authenticator):
        import os
        from PyQt5 import uic

        ui = os.path.join(os.path.dirname(__file__), 'authenticationdialog.ui')
        dlg = uic.loadUi(ui)
        dlg.adjustSize()
        dlg.siteDescription.setText("%s at %s" % (authenticator.realm(), self.url.host()))

        dlg.userEdit.setText(self.url.userName())
        dlg.passwordEdit.setText(self.url.password())

        if dlg.exec_() == QDialog.Accepted:
            authenticator.setUser(dlg.userEdit.text())
            authenticator.setPassword(dlg.passwordEdit.text())

    def sslErrors(self, reply, errors):
        errorString = ", ".join([str(error.errorString()) for error in errors])

        ret = QMessageBox.warning(self, "HTTP Example",
                "One or more SSL errors has occurred: %s" % errorString,
                QMessageBox.Ignore | QMessageBox.Abort)

        if ret == QMessageBox.Ignore:
            self.reply.ignoreSslErrors()
Exemple #31
0
class PackagesTable(QTableWidget):
    COL_NUMBER = 9

    def __init__(self, parent: QWidget, icon_cache: MemoryCache, download_icons: bool):
        super(PackagesTable, self).__init__()
        self.setObjectName('table_packages')
        self.setParent(parent)
        self.window = parent
        self.download_icons = download_icons
        self.setColumnCount(self.COL_NUMBER)
        self.setFocusPolicy(Qt.NoFocus)
        self.setShowGrid(False)
        self.verticalHeader().setVisible(False)
        self.horizontalHeader().setVisible(False)
        self.horizontalHeader().setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.setSelectionBehavior(QTableView.SelectRows)
        self.setHorizontalHeaderLabels(['' for _ in range(self.columnCount())])
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
        self.horizontalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))
        self.verticalScrollBar().setCursor(QCursor(Qt.PointingHandCursor))

        self.network_man = QNetworkAccessManager()
        self.network_man.finished.connect(self._load_icon_and_cache)

        self.icon_cache = icon_cache
        self.lock_async_data = Lock()
        self.setRowHeight(80, 80)
        self.cache_type_icon = {}
        self.i18n = self.window.i18n

    def icon_size(self) -> QSize:
        pixels = measure_based_on_height(0.02083)
        return QSize(pixels, pixels)

    def has_any_settings(self, pkg: PackageView):
        return pkg.model.has_history() or \
               pkg.model.can_be_downgraded() or \
               pkg.model.supports_ignored_updates() or \
               bool(pkg.model.get_custom_supported_actions())

    def show_pkg_actions(self, pkg: PackageView):
        menu_row = QMenu()
        menu_row.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        menu_row.setObjectName('app_actions')
        menu_row.setCursor(QCursor(Qt.PointingHandCursor))

        if pkg.model.installed:

            if pkg.model.has_history():
                def show_history():
                    self.window.begin_show_history(pkg)

                menu_row.addAction(QCustomMenuAction(parent=menu_row,
                                                     label=self.i18n["manage_window.apps_table.row.actions.history"],
                                                     action=show_history,
                                                     button_name='app_history'))

            if pkg.model.can_be_downgraded():

                def downgrade():
                    if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.downgrade'],
                                          body=self._parag(self.i18n[
                                                               'manage_window.apps_table.row.actions.downgrade.popup.body'].format(
                                              self._bold(str(pkg)))),
                                          i18n=self.i18n).ask():
                        self.window.begin_downgrade(pkg)

                menu_row.addAction(QCustomMenuAction(parent=menu_row,
                                                     label=self.i18n["manage_window.apps_table.row.actions.downgrade"],
                                                     action=downgrade,
                                                     button_name='app_downgrade'))

            if pkg.model.supports_ignored_updates():
                if pkg.model.is_update_ignored():
                    action_label = self.i18n["manage_window.apps_table.row.actions.ignore_updates_reverse"]
                    button_name = 'revert_ignore_updates'
                else:
                    action_label = self.i18n["manage_window.apps_table.row.actions.ignore_updates"]
                    button_name = 'ignore_updates'

                def ignore_updates():
                    self.window.begin_ignore_updates(pkg)

                menu_row.addAction(QCustomMenuAction(parent=menu_row,
                                                     label=action_label,
                                                     button_name=button_name,
                                                     action=ignore_updates))

        if bool(pkg.model.get_custom_supported_actions()):
            actions = [self._map_custom_action(pkg, a, menu_row) for a in pkg.model.get_custom_supported_actions()]
            menu_row.addActions(actions)

        menu_row.adjustSize()
        menu_row.popup(QCursor.pos())
        menu_row.exec_()

    def _map_custom_action(self, pkg: PackageView, action: CustomSoftwareAction, parent: QWidget) -> QCustomMenuAction:
        def custom_action():
            if action.i18n_confirm_key:
                body = self.i18n[action.i18n_confirm_key].format(bold(pkg.model.name))
            else:
                body = '{} ?'.format(self.i18n[action.i18n_label_key])

            if ConfirmationDialog(icon=QIcon(pkg.model.get_type_icon_path()),
                                  title=self.i18n[action.i18n_label_key],
                                  body=self._parag(body),
                                  i18n=self.i18n).ask():
                self.window.begin_execute_custom_action(pkg, action)

        return QCustomMenuAction(parent=parent,
                                 label=self.i18n[action.i18n_label_key],
                                 icon=QIcon(action.icon_path) if action.icon_path else None,
                                 action=custom_action)

    def refresh(self, pkg: PackageView):
        self._update_row(pkg, update_check_enabled=False, change_update_col=False)

    def update_package(self, pkg: PackageView, change_update_col: bool = False):
        if self.download_icons and pkg.model.icon_url:
            icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
            icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
            self.network_man.get(icon_request)

        self._update_row(pkg, change_update_col=change_update_col)

    def _uninstall(self, pkg: PackageView):
        if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'],
                              body=self._parag(
                                  self.i18n['manage_window.apps_table.row.actions.uninstall.popup.body'].format(
                                      self._bold(str(pkg)))),
                              i18n=self.i18n).ask():
            self.window.begin_uninstall(pkg)

    def _bold(self, text: str) -> str:
        return '<span style="font-weight: bold">{}</span>'.format(text)

    def _parag(self, text: str) -> str:
        return '<p>{}</p>'.format(text)

    def _install_app(self, pkgv: PackageView):

        body = self.i18n['manage_window.apps_table.row.actions.install.popup.body'].format(self._bold(str(pkgv)))

        warning = self.i18n.get('gem.{}.install.warning'.format(pkgv.model.get_type().lower()))

        if warning:
            body += '<br/><br/> {}'.format(
                '<br/>'.join(('{}.'.format(phrase) for phrase in warning.split('.') if phrase)))

        if ConfirmationDialog(title=self.i18n['manage_window.apps_table.row.actions.install.popup.title'],
                              body=self._parag(body),
                              i18n=self.i18n).ask():
            self.window.install(pkgv)

    def _load_icon_and_cache(self, http_response: QNetworkReply):
        icon_url = http_response.request().url().toString()

        icon_data = self.icon_cache.get(icon_url)
        icon_was_cached = True

        if not icon_data:
            icon_bytes = http_response.readAll()

            if not icon_bytes:
                return

            icon_was_cached = False
            pixmap = QPixmap()
            pixmap.loadFromData(icon_bytes)

            if not pixmap.isNull():
                icon = QIcon(pixmap)
                icon_data = {'icon': icon, 'bytes': icon_bytes}
                self.icon_cache.add(icon_url, icon_data)

        if icon_data:
            for idx, app in enumerate(self.window.pkgs):
                if app.model.icon_url == icon_url:
                    self._update_icon(self.cellWidget(idx, 0), icon_data['icon'])

                    if app.model.supports_disk_cache() and app.model.get_disk_icon_path() and icon_data['bytes']:
                        if not icon_was_cached or not os.path.exists(app.model.get_disk_icon_path()):
                            self.window.manager.cache_to_disk(pkg=app.model, icon_bytes=icon_data['bytes'],
                                                              only_icon=True)

    def update_packages(self, pkgs: List[PackageView], update_check_enabled: bool = True):
        self.setRowCount(0)  # removes the overwrite effect when updates the table
        self.setEnabled(True)

        if pkgs:
            self.setColumnCount(self.COL_NUMBER if update_check_enabled else self.COL_NUMBER - 1)
            self.setRowCount(len(pkgs))

            for idx, pkg in enumerate(pkgs):
                pkg.table_index = idx

                if self.download_icons and pkg.model.status == PackageStatus.READY and pkg.model.icon_url:
                    icon_request = QNetworkRequest(QUrl(pkg.model.icon_url))
                    icon_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
                    self.network_man.get(icon_request)

                self._update_row(pkg, update_check_enabled)

            self.scrollToTop()

    def _update_row(self, pkg: PackageView, update_check_enabled: bool = True, change_update_col: bool = True):
        self._set_col_icon(0, pkg)
        self._set_col_name(1, pkg)
        self._set_col_version(2, pkg)
        self._set_col_description(3, pkg)
        self._set_col_publisher(4, pkg)
        self._set_col_type(5, pkg)
        self._set_col_installed(6, pkg)
        self._set_col_actions(7, pkg)

        if change_update_col and update_check_enabled:
            if pkg.model.installed and not pkg.model.is_update_ignored() and pkg.model.update:
                col_update = QCustomToolbar()
                col_update.add_space()
                col_update.add_widget(UpgradeToggleButton(pkg=pkg,
                                                          root=self.window,
                                                          i18n=self.i18n,
                                                          checked=pkg.update_checked if pkg.model.can_be_updated() else False,
                                                          clickable=pkg.model.can_be_updated()))
                col_update.add_space()
            else:
                col_update = QLabel()

            self.setCellWidget(pkg.table_index, 8, col_update)

    def _gen_row_button(self, text: str, name: str, callback, tip: Optional[str] = None) -> QToolButton:
        col_bt = QToolButton()
        col_bt.setProperty('text_only', 'true')
        col_bt.setObjectName(name)
        col_bt.setCursor(QCursor(Qt.PointingHandCursor))
        col_bt.setText(text)
        col_bt.clicked.connect(callback)

        if tip:
            col_bt.setToolTip(tip)

        return col_bt

    def _set_col_installed(self, col: int, pkg: PackageView):
        toolbar = QCustomToolbar()
        toolbar.add_space()

        if pkg.model.installed:
            if pkg.model.can_be_uninstalled():
                def uninstall():
                    self._uninstall(pkg)

                item = self._gen_row_button(text=self.i18n['uninstall'].capitalize(),
                                            name='bt_uninstall',
                                            callback=uninstall,
                                            tip=self.i18n['manage_window.bt_uninstall.tip'])
            else:
                item = None

        elif pkg.model.can_be_installed():
            def install():
                self._install_app(pkg)

            item = self._gen_row_button(text=self.i18n['install'].capitalize(),
                                        name='bt_install',
                                        callback=install,
                                        tip=self.i18n['manage_window.bt_install.tip'])
        else:
            item = None

        toolbar.add_widget(item)
        toolbar.add_space()
        self.setCellWidget(pkg.table_index, col, toolbar)

    def _set_col_type(self, col: int, pkg: PackageView):
        icon_data = self.cache_type_icon.get(pkg.model.get_type())

        if icon_data is None:
            pixmap = QIcon(pkg.model.get_type_icon_path()).pixmap(self.icon_size())
            icon_data = {'px': pixmap, 'tip': '{}: {}'.format(self.i18n['type'], pkg.get_type_label())}
            self.cache_type_icon[pkg.model.get_type()] = icon_data

        col_type_icon = QLabel()
        col_type_icon.setCursor(QCursor(Qt.WhatsThisCursor))
        col_type_icon.setObjectName('app_type')
        col_type_icon.setProperty('icon', 'true')
        col_type_icon.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        col_type_icon.setPixmap(icon_data['px'])
        col_type_icon.setToolTip(icon_data['tip'])
        self.setCellWidget(pkg.table_index, col, col_type_icon)

    def _set_col_version(self, col: int, pkg: PackageView):
        label_version = QLabel(str(pkg.model.version if pkg.model.version else '?'))
        label_version.setObjectName('app_version')
        label_version.setAlignment(Qt.AlignCenter)

        item = QWidget()
        item.setProperty('container', 'true')
        item.setCursor(QCursor(Qt.WhatsThisCursor))
        item.setLayout(QHBoxLayout())
        item.layout().addWidget(label_version)

        if pkg.model.version:
            tooltip = self.i18n['version.installed'] if pkg.model.installed else self.i18n['version']
        else:
            tooltip = self.i18n['version.unknown']

        if pkg.model.update and not pkg.model.is_update_ignored():
            label_version.setProperty('update', 'true')
            tooltip = self.i18n['version.installed_outdated']

        if pkg.model.is_update_ignored():
            label_version.setProperty('ignored', 'true')
            tooltip = self.i18n['version.updates_ignored']

        if pkg.model.installed and pkg.model.update and not pkg.model.is_update_ignored() and pkg.model.version and pkg.model.latest_version and pkg.model.version != pkg.model.latest_version:
            tooltip = '{}. {}: {}'.format(tooltip, self.i18n['version.latest'], pkg.model.latest_version)
            label_version.setText(label_version.text() + '  >  {}'.format(pkg.model.latest_version))

        item.setToolTip(tooltip)
        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_icon(self, col: int, pkg: PackageView):
        icon_path = pkg.model.get_disk_icon_path()
        if pkg.model.installed and pkg.model.supports_disk_cache() and icon_path:
            if icon_path.startswith('/'):
                if os.path.isfile(icon_path):
                    with open(icon_path, 'rb') as f:
                        icon_bytes = f.read()
                        pixmap = QPixmap()
                        pixmap.loadFromData(icon_bytes)
                        icon = QIcon(pixmap)
                        self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': icon_bytes})
                else:
                    icon = QIcon(pkg.model.get_default_icon_path())
            else:
                try:
                    icon = QIcon.fromTheme(icon_path)

                    if icon.isNull():
                        icon = QIcon(pkg.model.get_default_icon_path())
                    else:
                        self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': None})

                except:
                    icon = QIcon(pkg.model.get_default_icon_path())

        elif not pkg.model.icon_url:
            icon = QIcon(pkg.model.get_default_icon_path())
        else:
            icon_data = self.icon_cache.get(pkg.model.icon_url)
            icon = icon_data['icon'] if icon_data else QIcon(pkg.model.get_default_icon_path())

        col_icon = QLabel()
        col_icon.setObjectName('app_icon')
        col_icon.setProperty('icon', 'true')
        col_icon.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        self._update_icon(col_icon, icon)
        self.setCellWidget(pkg.table_index, col, col_icon)

    def _set_col_name(self, col: int, pkg: PackageView):
        col_name = QLabel()
        col_name.setObjectName('app_name')
        col_name.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)
        col_name.setCursor(QCursor(Qt.WhatsThisCursor))

        name = pkg.model.get_display_name()
        if name:
            col_name.setToolTip('{}: {}'.format(self.i18n['app.name'].lower(), pkg.model.get_name_tooltip()))
        else:
            name = '...'
            col_name.setToolTip(self.i18n['app.name'].lower())

        if len(name) > NAME_MAX_SIZE:
            name = name[0:NAME_MAX_SIZE - 3] + '...'

        if len(name) < NAME_MAX_SIZE:
            name = name + ' ' * (NAME_MAX_SIZE - len(name))

        col_name.setText(name)
        self.setCellWidget(pkg.table_index, col, col_name)

    def _update_icon(self, label: QLabel, icon: QIcon):
        label.setPixmap(icon.pixmap(QSize(self.icon_size())))

    def _set_col_description(self, col: int, pkg: PackageView):
        item = QLabel()
        item.setObjectName('app_description')
        item.setCursor(QCursor(Qt.WhatsThisCursor))

        if pkg.model.description is not None or not pkg.model.is_application() or pkg.model.status == PackageStatus.READY:
            desc = pkg.model.description.split('\n')[0] if pkg.model.description else pkg.model.description
        else:
            desc = '...'

        if desc and desc != '...' and len(desc) > DESC_MAX_SIZE:
            desc = strip_html(desc[0: DESC_MAX_SIZE - 1]) + '...'

        item.setText(desc)

        if pkg.model.description:
            item.setToolTip(pkg.model.description)

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_publisher(self, col: int, pkg: PackageView):
        item = QToolBar()

        publisher = pkg.model.get_publisher()
        full_publisher = None

        if publisher:
            publisher = publisher.strip()
            full_publisher = publisher

            if len(publisher) > PUBLISHER_MAX_SIZE:
                publisher = full_publisher[0: PUBLISHER_MAX_SIZE - 3] + '...'

        lb_name = QLabel()
        lb_name.setObjectName('app_publisher')
        lb_name.setCursor(QCursor(Qt.WhatsThisCursor))

        if not publisher:
            if not pkg.model.installed:
                lb_name.setProperty('publisher_known', 'false')

            publisher = self.i18n['unknown']

        lb_name.setText('  {}'.format(publisher))
        item.addWidget(lb_name)

        if publisher and full_publisher:
            lb_name.setToolTip(
                self.i18n['publisher'].capitalize() + ((': ' + full_publisher) if full_publisher else ''))

            if pkg.model.is_trustable():
                lb_verified = QLabel()
                lb_verified.setObjectName('icon_publisher_verified')
                lb_verified.setCursor(QCursor(Qt.WhatsThisCursor))
                lb_verified.setToolTip(self.i18n['publisher.verified'].capitalize())
                item.addWidget(lb_verified)
            else:
                lb_name.setText(lb_name.text() + "   ")

        self.setCellWidget(pkg.table_index, col, item)

    def _set_col_actions(self, col: int, pkg: PackageView):
        toolbar = QCustomToolbar()
        toolbar.setObjectName('app_actions')
        toolbar.add_space()

        if pkg.model.installed:
            def run():
                self.window.begin_launch_package(pkg)

            bt = IconButton(i18n=self.i18n, action=run, tooltip=self.i18n['action.run.tooltip'])
            bt.setObjectName('app_run')

            if not pkg.model.can_be_run():
                bt.setEnabled(False)
                bt.setProperty('_enabled', 'false')

            toolbar.layout().addWidget(bt)

        settings = self.has_any_settings(pkg)

        if pkg.model.installed:
            def handle_custom_actions():
                self.show_pkg_actions(pkg)

            bt = IconButton(i18n=self.i18n, action=handle_custom_actions, tooltip=self.i18n['action.settings.tooltip'])
            bt.setObjectName('app_actions')
            bt.setEnabled(bool(settings))
            toolbar.layout().addWidget(bt)

        if not pkg.model.installed:
            def show_screenshots():
                self.window.begin_show_screenshots(pkg)

            bt = IconButton(i18n=self.i18n, action=show_screenshots,
                            tooltip=self.i18n['action.screenshots.tooltip'])
            bt.setObjectName('app_screenshots')

            if not pkg.model.has_screenshots():
                bt.setEnabled(False)
                bt.setProperty('_enabled', 'false')

            toolbar.layout().addWidget(bt)

        def show_info():
            self.window.begin_show_info(pkg)

        bt = IconButton(i18n=self.i18n, action=show_info, tooltip=self.i18n['action.info.tooltip'])
        bt.setObjectName('app_info')
        bt.setEnabled(bool(pkg.model.has_info()))
        toolbar.layout().addWidget(bt)

        self.setCellWidget(pkg.table_index, col, toolbar)

    def change_headers_policy(self, policy: QHeaderView = QHeaderView.ResizeToContents, maximized: bool = False):
        header_horizontal = self.horizontalHeader()
        for i in range(self.columnCount()):
            if maximized:
                if i not in (4, 5, 8):
                    header_horizontal.setSectionResizeMode(i, QHeaderView.ResizeToContents)
                else:
                    header_horizontal.setSectionResizeMode(i, QHeaderView.Stretch)
            else:
                header_horizontal.setSectionResizeMode(i, policy)

    def get_width(self):
        return reduce(operator.add, [self.columnWidth(i) for i in range(self.columnCount())])
Exemple #32
0
 def __init__(self, parent=None, flags=Qt.Dialog | Qt.WindowCloseButtonHint):
     super(Updater, self).__init__(parent, flags)
     self.parent = parent
     self.manager = QNetworkAccessManager(self)
     self.manager.finished.connect(self.done)
Exemple #33
0
    def __init__(self, url, version, parent=0, f=0):
        super(LauncherUpdateDialog, self).__init__(parent, f)

        self.updated = False
        self.url = url

        layout = QGridLayout()

        self.shown = False
        self.qnam = QNetworkAccessManager()
        self.http_reply = None

        progress_label = QLabel()
        progress_label.setText(_('Progress:'))
        layout.addWidget(progress_label, 0, 0, Qt.AlignRight)
        self.progress_label = progress_label

        progress_bar = QProgressBar()
        layout.addWidget(progress_bar, 0, 1)
        self.progress_bar = progress_bar

        url_label = QLabel()
        url_label.setText(_('Url:'))
        layout.addWidget(url_label, 1, 0, Qt.AlignRight)
        self.url_label = url_label

        url_lineedit = QLineEdit()
        url_lineedit.setText(url)
        url_lineedit.setReadOnly(True)
        layout.addWidget(url_lineedit, 1, 1)
        self.url_lineedit = url_lineedit

        size_label = QLabel()
        size_label.setText(_('Size:'))
        layout.addWidget(size_label, 2, 0, Qt.AlignRight)
        self.size_label = size_label

        size_value_label = QLabel()
        layout.addWidget(size_value_label, 2, 1)
        self.size_value_label = size_value_label

        speed_label = QLabel()
        speed_label.setText(_('Speed:'))
        layout.addWidget(speed_label, 3, 0, Qt.AlignRight)
        self.speed_label = speed_label

        speed_value_label = QLabel()
        layout.addWidget(speed_value_label, 3, 1)
        self.speed_value_label = speed_value_label

        cancel_button = QPushButton()
        cancel_button.setText(_('Cancel update'))
        cancel_button.setStyleSheet('font-size: 15px;')
        cancel_button.clicked.connect(self.cancel_update)
        layout.addWidget(cancel_button, 4, 0, 1, 2)
        self.cancel_button = cancel_button

        layout.setColumnStretch(1, 100)

        self.setLayout(layout)
        self.setMinimumSize(300, 0)
        self.setWindowTitle(_('CDDA Game Launcher self-update'))
class ComicVineTalker(QObject):

    logo_url = "http://static.comicvine.com/bundles/comicvinesite/images/logo.png"
    api_key = ""

    @staticmethod
    def getRateLimitMessage():
        if ComicVineTalker.api_key == "":
            return "Comic Vine rate limit exceeded.  You should configue your own Comic Vine API key."
        else:
            return "Comic Vine rate limit exceeded.  Please wait a bit."

    def __init__(self):
        QObject.__init__(self)

        self.api_base_url = "https://comicvine.gamespot.com/api"
        self.wait_for_rate_limit = False

        # key that is registered to comictagger
        default_api_key = '27431e6787042105bd3e47e169a624521f89f3a4'

        if ComicVineTalker.api_key == "":
            self.api_key = default_api_key
        else:
            self.api_key = ComicVineTalker.api_key

        self.log_func = None

        # always use a tls context for urlopen
        self.ssl = ssl.SSLContext(ssl.PROTOCOL_TLS)

    def setLogFunc(self, log_func):
        self.log_func = log_func

    def writeLog(self, text):
        if self.log_func is None:
            # sys.stdout.write(text.encode(errors='replace'))
            # sys.stdout.flush()
            print(text, file=sys.stderr)
        else:
            self.log_func(text)

    def parseDateStr(self, date_str):
        day = None
        month = None
        year = None
        if date_str is not None:
            parts = date_str.split('-')
            year = parts[0]
            if len(parts) > 1:
                month = parts[1]
                if len(parts) > 2:
                    day = parts[2]
        return day, month, year

    def testKey(self, key):

        try:
            test_url = self.api_base_url + "/issue/1/?api_key=" + \
                key + "&format=json&field_list=name"
            resp = urllib.request.urlopen(test_url, context=self.ssl)
            content = resp.read()
    
            cv_response = json.loads(content.decode('utf-8'))
    
            # Bogus request, but if the key is wrong, you get error 100: "Invalid
            # API Key"
            return cv_response['status_code'] != 100
        except:
            return False

    """
    Get the contect from the CV server.  If we're in "wait mode" and status code is a rate limit error
    sleep for a bit and retry.
    """

    def getCVContent(self, url):
        total_time_waited = 0
        limit_wait_time = 1
        counter = 0
        wait_times = [1, 2, 3, 4]
        while True:
            content = self.getUrlContent(url)
            cv_response = json.loads(content.decode('utf-8'))
            if self.wait_for_rate_limit and cv_response[
                    'status_code'] == ComicVineTalkerException.RateLimit:
                self.writeLog(
                    "Rate limit encountered.  Waiting for {0} minutes\n".format(limit_wait_time))
                time.sleep(limit_wait_time * 60)
                total_time_waited += limit_wait_time
                limit_wait_time = wait_times[counter]
                if counter < 3:
                    counter += 1
                # don't wait much more than 20 minutes
                if total_time_waited < 20:
                    continue
            if cv_response['status_code'] != 1:
                self.writeLog(
                    "Comic Vine query failed with error #{0}:  [{1}]. \n".format(
                        cv_response['status_code'],
                        cv_response['error']))
                raise ComicVineTalkerException(
                    cv_response['status_code'], cv_response['error'])
            else:
                # it's all good
                break
        return cv_response

    def getUrlContent(self, url):
        # connect to server:
        #  if there is a 500 error, try a few more times before giving up
        #  any other error, just bail
        #print("---", url)
        for tries in range(3):
            try:
                resp = urllib.request.urlopen(url, context=self.ssl)
                return resp.read()
            except urllib.error.HTTPError as e:
                if e.getcode() == 500:
                    self.writeLog("Try #{0}: ".format(tries + 1))
                    time.sleep(1)
                self.writeLog(str(e) + "\n")

                if e.getcode() != 500:
                    break

            except Exception as e:
                self.writeLog(str(e) + "\n")
                raise ComicVineTalkerException(
                    ComicVineTalkerException.Network, "Network Error!")

        raise ComicVineTalkerException(
            ComicVineTalkerException.Unknown, "Error on Comic Vine server")

    def searchForSeries(self, series_name, callback=None, refresh_cache=False):

        # remove cruft from the search string
        series_name = utils.removearticles(series_name).lower().strip()

        # before we search online, look in our cache, since we might have
        # done this same search recently
        cvc = ComicVineCacher()
        if not refresh_cache:
            cached_search_results = cvc.get_search_results(series_name)

            if len(cached_search_results) > 0:
                return cached_search_results

        original_series_name = series_name

        # Split and rejoin to remove extra internal spaces
        query_word_list = series_name.split()
        query_string = " ".join( query_word_list ).strip()
        #print ("Query string = ", query_string)

        query_string = urllib.parse.quote_plus(query_string.encode("utf-8"))

        search_url = self.api_base_url + "/search/?api_key=" + self.api_key + "&format=json&resources=volume&query=" + \
            query_string + \
            "&field_list=name,id,start_year,publisher,image,description,count_of_issues&limit=100"
        cv_response = self.getCVContent(search_url + "&page=1")

        search_results = list()

        # see http://api.comicvine.com/documentation/#handling_responses

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']

        # 8 Dec 2018 - Comic Vine changed query results again. Terms are now
        # ORed together, and we get thousands of results.  Good news is the
        # results are sorted by relevance, so we can be smart about halting
        # the search.  
        # 1. Don't fetch more than some sane amount of pages.
        max_results = 500 
        # 2. Halt when not all of our search terms are present in a result
        # 3. Halt when the results contain more (plus threshold) words than
        #    our search
        result_word_count_max = len(query_word_list) + 3

        total_result_count = min(total_result_count, max_results) 

        if callback is None:
            self.writeLog(
                "Found {0} of {1} results\n".format(
                    cv_response['number_of_page_results'],
                    cv_response['number_of_total_results']))
        search_results.extend(cv_response['results'])
        page = 1

        if callback is not None:
            callback(current_result_count, total_result_count)

        # see if we need to keep asking for more pages...
        stop_searching = False
        while (current_result_count < total_result_count):

            last_result = search_results[-1]['name']

            # See if the last result's name has all the of the search terms.
            # if not, break out of this, loop, we're done.
            #print("Searching for {} in '{}'".format(query_word_list, last_result))
            for term in query_word_list:
                if term not in last_result.lower():
                    #print("Term '{}' not in last result. Halting search result fetching".format(term))
                    stop_searching = True
                    break

            # Also, stop searching when the word count of last results is too much longer
            # than our search terms list 
            if len(utils.removearticles(last_result).split()) > result_word_count_max:
                #print("Last result '{}' is too long. Halting search result fetching".format(last_result))
                stop_searching = True

            if stop_searching:
                break

            if callback is None:
                self.writeLog(
                    "getting another page of results {0} of {1}...\n".format(
                        current_result_count,
                        total_result_count))
            page += 1

            cv_response = self.getCVContent(search_url + "&page=" + str(page))

            search_results.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

            if callback is not None:
                callback(current_result_count, total_result_count)

        # Remove any search results that don't contain all the search terms
        # (iterate backwards for easy removal)
        for i in range(len(search_results) - 1, -1, -1):
            record = search_results[i]
            for term in query_word_list:
                if term not in record['name'].lower():
                    del search_results[i]
                    break

        # for record in search_results:
            #print(u"{0}: {1} ({2})".format(record['id'], record['name'] , record['start_year']))
            # print(record)
            #record['count_of_issues'] = record['count_of_isssues']
        #print(u"{0}: {1} ({2})".format(search_results['results'][0]['id'], search_results['results'][0]['name'] , search_results['results'][0]['start_year']))

        # cache these search results
        cvc.add_search_results(original_series_name, search_results)

        return search_results

    def fetchVolumeData(self, series_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        cached_volume_result = cvc.get_volume_info(series_id)

        if cached_volume_result is not None:
            return cached_volume_result

        volume_url = self.api_base_url + "/volume/" + CVTypeID.Volume + "-" + \
            str(series_id) + "/?api_key=" + self.api_key + \
            "&field_list=name,id,start_year,publisher,count_of_issues&format=json"

        cv_response = self.getCVContent(volume_url)

        volume_results = cv_response['results']

        cvc.add_volume_info(volume_results)

        return volume_results

    def fetchIssuesByVolume(self, series_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        cached_volume_issues_result = cvc.get_volume_issues_info(series_id)

        if cached_volume_issues_result is not None:
            return cached_volume_issues_result

        #---------------------------------
        issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + "&filter=volume:" + \
            str(series_id) + \
            "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"
        cv_response = self.getCVContent(issues_url)

        #------------------------------------

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']
        #print("total_result_count", total_result_count)

        #print("Found {0} of {1} results".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
        volume_issues_result = cv_response['results']
        page = 1
        offset = 0

        # see if we need to keep asking for more pages...
        while (current_result_count < total_result_count):
            #print("getting another page of issue results {0} of {1}...".format(current_result_count, total_result_count))
            page += 1
            offset += cv_response['number_of_page_results']

            # print issues_url+ "&offset="+str(offset)
            cv_response = self.getCVContent(
                issues_url + "&offset=" + str(offset))

            volume_issues_result.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

        self.repairUrls(volume_issues_result)

        cvc.add_volume_issues_info(series_id, volume_issues_result)

        return volume_issues_result

    def fetchIssuesByVolumeIssueNumAndYear(
            self, volume_id_list, issue_number, year):
        volume_filter = "volume:"
        for vid in volume_id_list:
            volume_filter += str(vid) + "|"

        year_filter = ""
        if year is not None and str(year).isdigit():
            year_filter = ",cover_date:{0}-1-1|{1}-1-1".format(
                year, int(year) + 1)

        issue_number = urllib.parse.quote_plus(str(issue_number).encode("utf-8"))

        filter = "&filter=" + volume_filter + \
            year_filter + ",issue_number:" + issue_number

        issues_url = self.api_base_url + "/issues/" + "?api_key=" + self.api_key + filter + \
            "&field_list=id,volume,issue_number,name,image,cover_date,site_detail_url,description&format=json"

        cv_response = self.getCVContent(issues_url)

        #------------------------------------

        limit = cv_response['limit']
        current_result_count = cv_response['number_of_page_results']
        total_result_count = cv_response['number_of_total_results']
        #print("total_result_count", total_result_count)

        #print("Found {0} of {1} results\n".format(cv_response['number_of_page_results'], cv_response['number_of_total_results']))
        filtered_issues_result = cv_response['results']
        page = 1
        offset = 0

        # see if we need to keep asking for more pages...
        while (current_result_count < total_result_count):
            #print("getting another page of issue results {0} of {1}...\n".format(current_result_count, total_result_count))
            page += 1
            offset += cv_response['number_of_page_results']

            # print issues_url+ "&offset="+str(offset)
            cv_response = self.getCVContent(
                issues_url + "&offset=" + str(offset))

            filtered_issues_result.extend(cv_response['results'])
            current_result_count += cv_response['number_of_page_results']

        self.repairUrls(filtered_issues_result)

        return filtered_issues_result

    def fetchIssueData(self, series_id, issue_number, settings):

        volume_results = self.fetchVolumeData(series_id)
        issues_list_results = self.fetchIssuesByVolume(series_id)

        found = False
        for record in issues_list_results:
            if IssueString(issue_number).asString() is None:
                issue_number = 1
            if IssueString(record['issue_number']).asString().lower() == IssueString(
                    issue_number).asString().lower():
                found = True
                break

        if (found):
            issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
                str(record['id']) + "/?api_key=" + \
                self.api_key + "&format=json"

            cv_response = self.getCVContent(issue_url)
            issue_results = cv_response['results']

        else:
            return None

        # Now, map the Comic Vine data to generic metadata
        return self.mapCVDataToMetadata(
            volume_results, issue_results, settings)

    def fetchIssueDataByIssueID(self, issue_id, settings):

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + "&format=json"
        cv_response = self.getCVContent(issue_url)

        issue_results = cv_response['results']

        volume_results = self.fetchVolumeData(issue_results['volume']['id'])

        # Now, map the Comic Vine data to generic metadata
        md = self.mapCVDataToMetadata(volume_results, issue_results, settings)
        md.isEmpty = False
        return md

    def mapCVDataToMetadata(self, volume_results, issue_results, settings):

        # Now, map the Comic Vine data to generic metadata
        metadata = GenericMetadata()

        metadata.series = issue_results['volume']['name']

        num_s = IssueString(issue_results['issue_number']).asString()
        metadata.issue = num_s
        metadata.title = issue_results['name']

        metadata.publisher = volume_results['publisher']['name']
        metadata.day, metadata.month, metadata.year = self.parseDateStr(
            issue_results['cover_date'])

        #metadata.issueCount = volume_results['count_of_issues']
        metadata.comments = self.cleanup_html(
            issue_results['description'], settings.remove_html_tables)
        if settings.use_series_start_as_volume:
            metadata.volume = volume_results['start_year']

        metadata.notes = "Tagged with the {0} fork of ComicTagger {1} using info from Comic Vine on {2}.  [Issue ID {3}]".format(
            ctversion.fork,
            ctversion.version,
            datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            issue_results['id'])

        metadata.webLink = issue_results['site_detail_url']

        person_credits = issue_results['person_credits']
        for person in person_credits:
            if 'role' in person:
                roles = person['role'].split(',')
                for role in roles:
                    # can we determine 'primary' from CV??
                    metadata.addCredit(
                        person['name'], role.title().strip(), False)

        character_credits = issue_results['character_credits']
        character_list = list()
        for character in character_credits:
            character_list.append(character['name'])
        metadata.characters = utils.listToString(character_list)

        team_credits = issue_results['team_credits']
        team_list = list()
        for team in team_credits:
            team_list.append(team['name'])
        metadata.teams = utils.listToString(team_list)

        location_credits = issue_results['location_credits']
        location_list = list()
        for location in location_credits:
            location_list.append(location['name'])
        metadata.locations = utils.listToString(location_list)

        story_arc_credits = issue_results['story_arc_credits']
        arc_list = []
        for arc in story_arc_credits:
            arc_list.append(arc['name'])
        if len(arc_list) > 0:
            metadata.storyArc = utils.listToString(arc_list)

        return metadata

    def cleanup_html(self, string, remove_html_tables):
        """
        converter = html2text.HTML2Text()
        #converter.emphasis_mark = '*'
        #converter.ignore_links = True
        converter.body_width = 0

        print(html2text.html2text(string))
        return string
        #return converter.handle(string)
        """

        if string is None:
            return ""
        # find any tables
        soup = BeautifulSoup(string, "html.parser")
        tables = soup.findAll('table')

        # remove all newlines first
        string = string.replace("\n", "")

        # put in our own
        string = string.replace("<br>", "\n")
        string = string.replace("</p>", "\n\n")
        string = string.replace("<h4>", "*")
        string = string.replace("</h4>", "*\n")

        # remove the tables
        p = re.compile(r'<table[^<]*?>.*?<\/table>')
        if remove_html_tables:
            string = p.sub('', string)
            string = string.replace("*List of covers and their creators:*", "")
        else:
            string = p.sub('{}', string)

        # now strip all other tags
        p = re.compile(r'<[^<]*?>')
        newstring = p.sub('', string)

        newstring = newstring.replace('&nbsp;', ' ')
        newstring = newstring.replace('&amp;', '&')

        newstring = newstring.strip()

        if not remove_html_tables:
            # now rebuild the tables into text from BSoup
            try:
                table_strings = []
                for table in tables:
                    rows = []
                    hdrs = []
                    col_widths = []
                    for hdr in table.findAll('th'):
                        item = hdr.string.strip()
                        hdrs.append(item)
                        col_widths.append(len(item))
                    rows.append(hdrs)

                    for row in table.findAll('tr'):
                        cols = []
                        col = row.findAll('td')
                        i = 0
                        for c in col:
                            item = c.string.strip()
                            cols.append(item)
                            if len(item) > col_widths[i]:
                                col_widths[i] = len(item)
                            i += 1
                        if len(cols) != 0:
                            rows.append(cols)
                    # now we have the data, make it into text
                    fmtstr = ""
                    for w in col_widths:
                        fmtstr += " {{:{}}}|".format(w + 1)
                    width = sum(col_widths) + len(col_widths) * 2
                    print("width=", width)
                    table_text = ""
                    counter = 0
                    for row in rows:
                        table_text += fmtstr.format(*row) + "\n"
                        if counter == 0 and len(hdrs) != 0:
                            table_text += "-" * width + "\n"
                        counter += 1

                    table_strings.append(table_text)

                newstring = newstring.format(*table_strings)
            except:
                # we caught an error rebuilding the table.
                # just bail and remove the formatting
                print("table parse error")
                newstring.replace("{}", "")

        return newstring

    def fetchIssueDate(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        day, month, year = self.parseDateStr(details['cover_date'])
        return month, year

    def fetchIssueCoverURLs(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        return details['image_url'], details['thumb_image_url']

    def fetchIssuePageURL(self, issue_id):
        details = self.fetchIssueSelectDetails(issue_id)
        return details['site_detail_url']

    def fetchIssueSelectDetails(self, issue_id):

        #cached_image_url,cached_thumb_url,cached_month,cached_year = self.fetchCachedIssueSelectDetails(issue_id)
        cached_details = self.fetchCachedIssueSelectDetails(issue_id)
        if cached_details['image_url'] is not None:
            return cached_details

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + \
            "&format=json&field_list=image,cover_date,site_detail_url"

        details = dict()
        details['image_url'] = None
        details['thumb_image_url'] = None
        details['cover_date'] = None
        details['site_detail_url'] = None

        cv_response = self.getCVContent(issue_url)

        details['image_url'] = cv_response['results']['image']['super_url']
        details['thumb_image_url'] = cv_response[
            'results']['image']['thumb_url']
        details['cover_date'] = cv_response['results']['cover_date']
        details['site_detail_url'] = cv_response['results']['site_detail_url']

        if details['image_url'] is not None:
            self.cacheIssueSelectDetails(issue_id,
                                         details['image_url'],
                                         details['thumb_image_url'],
                                         details['cover_date'],
                                         details['site_detail_url'])
        # print(details['site_detail_url'])
        return details

    def fetchCachedIssueSelectDetails(self, issue_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        return cvc.get_issue_select_details(issue_id)

    def cacheIssueSelectDetails(
            self, issue_id, image_url, thumb_url, cover_date, page_url):
        cvc = ComicVineCacher()
        cvc.add_issue_select_details(
            issue_id, image_url, thumb_url, cover_date, page_url)

    def fetchAlternateCoverURLs(self, issue_id, issue_page_url):
        url_list = self.fetchCachedAlternateCoverURLs(issue_id)
        if url_list is not None:
            return url_list

        # scrape the CV issue page URL to get the alternate cover URLs
        resp = urllib.request.urlopen(issue_page_url, context=self.ssl)
        content = resp.read()
        alt_cover_url_list = self.parseOutAltCoverUrls(content)

        # cache this alt cover URL list
        self.cacheAlternateCoverURLs(issue_id, alt_cover_url_list)

        return alt_cover_url_list

    def parseOutAltCoverUrls(self, page_html):
        soup = BeautifulSoup(page_html, "html.parser")
    
        alt_cover_url_list = []
    
        # Using knowledge of the layout of the Comic Vine issue page here:
        # look for the divs that are in the classes 'imgboxart' and
        # 'issue-cover'
        div_list = soup.find_all('div')
        covers_found = 0
        for d in div_list:
            if 'class' in d.attrs:
                c = d['class']
                if ('imgboxart' in c and 
                        'issue-cover' in c and
                        d.img['src'].startswith("http")
                   ):
                    
                    covers_found += 1
                    if covers_found != 1:
                            alt_cover_url_list.append(d.img['src'])
    
        return alt_cover_url_list

    def fetchCachedAlternateCoverURLs(self, issue_id):

        # before we search online, look in our cache, since we might already
        # have this info
        cvc = ComicVineCacher()
        url_list = cvc.get_alt_covers(issue_id)
        if url_list is not None:
            return url_list
        else:
            return None

    def cacheAlternateCoverURLs(self, issue_id, url_list):
        cvc = ComicVineCacher()
        cvc.add_alt_covers(issue_id, url_list)

    #-------------------------------------------------------------------------
    urlFetchComplete = pyqtSignal(str, str, int)

    def asyncFetchIssueCoverURLs(self, issue_id):

        self.issue_id = issue_id
        details = self.fetchCachedIssueSelectDetails(issue_id)
        if details['image_url'] is not None:
            self.urlFetchComplete.emit(
                details['image_url'],
                details['thumb_image_url'],
                self.issue_id)
            return

        issue_url = self.api_base_url + "/issue/" + CVTypeID.Issue + "-" + \
            str(issue_id) + "/?api_key=" + self.api_key + \
            "&format=json&field_list=image,cover_date,site_detail_url"
        self.nam = QNetworkAccessManager()
        self.nam.finished.connect(self.asyncFetchIssueCoverURLComplete)
        self.nam.get(QNetworkRequest(QUrl(issue_url)))

    def asyncFetchIssueCoverURLComplete(self, reply):

        # read in the response
        data = reply.readAll()

        try:
            cv_response = json.loads(bytes(data))
        except Exception as e:
            print("Comic Vine query failed to get JSON data", file=sys.stderr)
            print(str(data), file=sys.stderr)
            return

        if cv_response['status_code'] != 1:
            print("Comic Vine query failed with error:  [{0}]. ".format(
                cv_response['error']), file=sys.stderr)
            return

        image_url = cv_response['results']['image']['super_url']
        thumb_url = cv_response['results']['image']['thumb_url']
        cover_date = cv_response['results']['cover_date']
        page_url = cv_response['results']['site_detail_url']

        self.cacheIssueSelectDetails(
            self.issue_id, image_url, thumb_url, cover_date, page_url)

        self.urlFetchComplete.emit(image_url, thumb_url, self.issue_id)

    altUrlListFetchComplete = pyqtSignal(list, int)

    def asyncFetchAlternateCoverURLs(self, issue_id, issue_page_url):
        # This async version requires the issue page url to be provided!
        self.issue_id = issue_id
        url_list = self.fetchCachedAlternateCoverURLs(issue_id)
        if url_list is not None:
            self.altUrlListFetchComplete.emit(url_list, int(self.issue_id))
            return

        self.nam = QNetworkAccessManager()
        self.nam.finished.connect(self.asyncFetchAlternateCoverURLsComplete)
        self.nam.get(QNetworkRequest(QUrl(str(issue_page_url))))

    def asyncFetchAlternateCoverURLsComplete(self, reply):
        # read in the response
        html = str(reply.readAll())
        alt_cover_url_list = self.parseOutAltCoverUrls(html)

        # cache this alt cover URL list
        self.cacheAlternateCoverURLs(self.issue_id, alt_cover_url_list)

        self.altUrlListFetchComplete.emit(
            alt_cover_url_list, int(self.issue_id))

    def repairUrls(self, issue_list):
        # make sure there are URLs for the image fields
        for issue in issue_list:
            if issue['image'] is None:
                issue['image'] = dict()
                issue['image']['super_url'] = ComicVineTalker.logo_url
                issue['image']['thumb_url'] = ComicVineTalker.logo_url
Exemple #35
0
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._zero_conf_browser = None

        self._application = CuraApplication.getInstance()
        self._api = self._application.getCuraAPI()

        # Create a cloud output device manager that abstracts all cloud connection logic away.
        self._cloud_output_device_manager = CloudOutputDeviceManager()

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        self._application.globalContainerStackChanged.connect(
            self.refreshConnections)

        self._discovered_devices = {}

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")
        self._min_cloud_version = Version("5.2.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = self._application.getPreferences()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  # A comma-separated list of ip adresses or hostnames

        manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")
        self._manual_instances = {
            address: ManualPrinterRequest(address)
            for address in manual_instances
        }  # type: Dict[str, ManualPrinterRequest]

        # Store the last manual entry key
        self._last_manual_entry_key = ""  # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(
            target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._account = self._api.account

        # Check if cloud flow is possible when user logs in
        self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)

        # Check if cloud flow is possible when user switches machines
        self._application.globalContainerStackChanged.connect(
            self._onMachineSwitched)

        # Listen for when cloud flow is possible
        self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)

        # Listen if cloud cluster was added
        self._cloud_output_device_manager.addedCloudCluster.connect(
            self._onCloudPrintingConfigured)

        # Listen if cloud cluster was removed
        self._cloud_output_device_manager.removedCloudCluster.connect(
            self.checkCloudFlowIsPossible)

        self._start_cloud_flow_message = None  # type: Optional[Message]
        self._cloud_flow_complete_message = None  # type: Optional[Message]
Exemple #36
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal(
    )  # Called '...Signal' to avoid confusion with function-names.
    removeDeviceSignal = Signal()  # Ditto ^^^.
    discoveredDevicesChanged = Signal()
    cloudFlowIsPossible = Signal()

    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._zero_conf_browser = None

        self._application = CuraApplication.getInstance()
        self._api = self._application.getCuraAPI()

        # Create a cloud output device manager that abstracts all cloud connection logic away.
        self._cloud_output_device_manager = CloudOutputDeviceManager()

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        self._application.globalContainerStackChanged.connect(
            self.refreshConnections)

        self._discovered_devices = {}

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")
        self._min_cloud_version = Version("5.2.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = self._application.getPreferences()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  # A comma-separated list of ip adresses or hostnames

        manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")
        self._manual_instances = {
            address: ManualPrinterRequest(address)
            for address in manual_instances
        }  # type: Dict[str, ManualPrinterRequest]

        # Store the last manual entry key
        self._last_manual_entry_key = ""  # type: str

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(
            target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

        self._account = self._api.account

        # Check if cloud flow is possible when user logs in
        self._account.loginStateChanged.connect(self.checkCloudFlowIsPossible)

        # Check if cloud flow is possible when user switches machines
        self._application.globalContainerStackChanged.connect(
            self._onMachineSwitched)

        # Listen for when cloud flow is possible
        self.cloudFlowIsPossible.connect(self._onCloudFlowPossible)

        # Listen if cloud cluster was added
        self._cloud_output_device_manager.addedCloudCluster.connect(
            self._onCloudPrintingConfigured)

        # Listen if cloud cluster was removed
        self._cloud_output_device_manager.removedCloudCluster.connect(
            self.checkCloudFlowIsPossible)

        self._start_cloud_flow_message = None  # type: Optional[Message]
        self._cloud_flow_complete_message = None  # type: Optional[Message]

    def getDiscoveredDevices(self):
        return self._discovered_devices

    def getLastManualDevice(self) -> str:
        return self._last_manual_entry_key

    def resetLastManualDevice(self) -> None:
        self._last_manual_entry_key = ""

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()
        self._cloud_output_device_manager.start()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(
            self._zero_conf, u'_ultimaker._tcp.local.',
            [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)
        self.resetLastManualDevice()

    # TODO: CHANGE TO HOSTNAME
    def refreshConnections(self):
        active_machine = self._application.getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    # It should already be set, but if it actually connects we know for sure it's supported!
                    active_machine.addConfiguredConnectionType(
                        self._discovered_devices[key].connectionType.value)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[
                        key].connectionStateChanged.connect(
                            self._onDeviceConnectionStateChanged)
                else:
                    self._onDeviceConnectionStateChanged(key)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log(
                        "d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[
                        key].connectionStateChanged.disconnect(
                            self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            # Sometimes the status changes after changing the global container and maybe the device doesn't belong to this machine
            um_network_key = self._application.getGlobalContainerStack(
            ).getMetaDataEntry("um_network_key")
            if key == um_network_key:
                self.getOutputDeviceManager().addOutputDevice(
                    self._discovered_devices[key])
                self.checkCloudFlowIsPossible(None)
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()
        self._cloud_output_device_manager.stop()

    def canAddManualDevice(self,
                           address: str = "") -> ManualDeviceAdditionAttempt:
        # This plugin should always be the fallback option (at least try it):
        return ManualDeviceAdditionAttempt.POSSIBLE

    def removeManualDevice(self,
                           key: str,
                           address: Optional[str] = None) -> None:
        if key not in self._discovered_devices and address is not None:
            key = "manual:%s" % address

        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)
            self.resetLastManualDevice()

        if address in self._manual_instances:
            manual_printer_request = self._manual_instances.pop(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances.keys()))

            if manual_printer_request.network_reply is not None:
                manual_printer_request.network_reply.abort()

            if manual_printer_request.callback is not None:
                self._application.callLater(manual_printer_request.callback,
                                            False, address)

    def addManualDevice(
            self,
            address: str,
            callback: Optional[Callable[[bool, str], None]] = None) -> None:
        if address in self._manual_instances:
            Logger.log(
                "i",
                "Manual printer with address [%s] has already been added, do nothing",
                address)
            return

        self._manual_instances[address] = ManualPrinterRequest(
            address, callback=callback)
        self._preferences.setValue("um3networkprinting/manual_instances",
                                   ",".join(self._manual_instances.keys()))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true",
            b"temporary":
            b"true"  # Still a temporary device until all the info is retrieved in _onNetworkRequestFinished
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)
        self._last_manual_entry_key = instance_name

        reply = self._checkManualDevice(address)
        self._manual_instances[address].network_reply = reply

    def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
        discovered_device = self._discovered_devices.get(key)
        if discovered_device is None:
            Logger.log("e", "Could not find discovered device with key [%s]",
                       key)
            return

        group_name = discovered_device.getProperty("name")
        machine_type_id = discovered_device.getProperty("printer_type")

        Logger.log(
            "i",
            "Creating machine from network device with key = [%s], group name = [%s],  printer type = [%s]",
            key, group_name, machine_type_id)

        self._application.getMachineManager().addMachine(
            machine_type_id, group_name)

        # connect the new machine to that network printer
        self._api.machines.addOutputDeviceToCurrentMachine(discovered_device)

        # ensure that the connection states are refreshed.
        self.refreshConnections()

    def _checkManualDevice(self, address: str) -> Optional[QNetworkReply]:
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        return self._network_manager.get(name_request)

    ##  This is the function which handles the above network request's reply when it comes back.
    def _onNetworkRequestFinished(self, reply: "QNetworkReply") -> None:
        reply_url = reply.url().toString()

        address = reply.url().host()
        device = None
        properties = {}  # type: Dict[bytes, bytes]

        if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
            # Either:
            #  - Something went wrong with checking the firmware version!
            #  - Something went wrong with checking the amount of printers the cluster has!
            #  - Couldn't find printer at the address when trying to add it manually.
            if address in self._manual_instances:
                key = "manual:" + address
                self.removeManualDevice(key, address)
            return

        if "system" in reply_url:
            try:
                system_info = json.loads(
                    bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            if address in self._manual_instances:
                manual_printer_request = self._manual_instances[address]
                manual_printer_request.network_reply = None
                if manual_printer_request.callback is not None:
                    self._application.callLater(
                        manual_printer_request.callback, True, address)

            has_cluster_capable_firmware = Version(
                system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": (system_info["name"] + " (manual)").encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine":
                str(system_info['hardware']["typeid"]).encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address +
                                   self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(
                    bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b"cluster_size"] = str(
                    len(cluster_printers_list)).encode("utf-8")
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id: str) -> None:
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(
                        self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass
            self._application.getDiscoveredPrintersModel(
            ).removeDiscoveredPrinter(device.address)
            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        printer_type = properties.get(b"machine", b"").decode("utf-8")
        printer_type_identifiers = {
            "9066": "ultimaker3",
            "9511": "ultimaker3_extended",
            "9051": "ultimaker_s5"
        }

        for key, value in printer_type_identifiers.items():
            if printer_type.startswith(key):
                properties[b"printer_type"] = bytes(value, encoding="utf8")
                break
        else:
            properties[b"printer_type"] = b"Unknown"
        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(
                name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(
                name, address, properties)
        self._application.getDiscoveredPrintersModel().addDiscoveredPrinter(
            address, device.getId(), properties[b"name"].decode("utf-8"),
            self._createMachineFromDiscoveredPrinter,
            properties[b"printer_type"].decode("utf-8"), device)
        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = self._application.getGlobalContainerStack()
        if global_container_stack and device.getId(
        ) == global_container_stack.getMetaDataEntry("um_network_key"):
            # Ensure that the configured connection type is set.
            global_container_stack.addConfiguredConnectionType(
                device.connectionType.value)
            device.connect()
            device.connectionStateChanged.connect(
                self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name,
                                     state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout=5.0)

            # Stop if the application is shutting down
            if self._application.isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = [
            ]  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type,
                                                    name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException(
                        "e",
                        "Failed to get service info for [%s] [%s], the request will be rescheduled",
                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be careful
    #   calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address,
                                                  info.properties)
                    else:
                        Logger.log(
                            "w",
                            "The type of the found device is '%s', not 'printer'! Ignoring.."
                            % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True

    ## Check if the prerequsites are in place to start the cloud flow
    def checkCloudFlowIsPossible(self,
                                 cluster: Optional[CloudOutputDevice]) -> None:
        Logger.log("d", "Checking if cloud connection is possible...")

        # Pre-Check: Skip if active machine already has been cloud connected or you said don't ask again
        active_machine = self._application.getMachineManager(
        ).activeMachine  # type: Optional[GlobalStack]
        if active_machine:
            # Check 1A: Printer isn't already configured for cloud
            if ConnectionType.CloudConnection.value in active_machine.configuredConnectionTypes:
                Logger.log("d",
                           "Active machine was already configured for cloud.")
                return

            # Check 1B: Printer isn't already configured for cloud
            if active_machine.getMetaDataEntry("cloud_flow_complete", False):
                Logger.log("d",
                           "Active machine was already configured for cloud.")
                return

            # Check 2: User did not already say "Don't ask me again"
            if active_machine.getMetaDataEntry("do_not_show_cloud_message",
                                               False):
                Logger.log(
                    "d", "Active machine shouldn't ask about cloud anymore.")
                return

            # Check 3: User is logged in with an Ultimaker account
            if not self._account.isLoggedIn:
                Logger.log("d", "Cloud Flow not possible: User not logged in!")
                return

            # Check 4: Machine is configured for network connectivity
            if not self._application.getMachineManager(
            ).activeMachineHasNetworkConnection:
                Logger.log(
                    "d", "Cloud Flow not possible: Machine is not connected!")
                return

            # Check 5: Machine has correct firmware version
            firmware_version = self._application.getMachineManager(
            ).activeMachineFirmwareVersion  # type: str
            if not Version(firmware_version) > self._min_cloud_version:
                Logger.log(
                    "d",
                    "Cloud Flow not possible: Machine firmware (%s) is too low! (Requires version %s)",
                    firmware_version, self._min_cloud_version)
                return

            Logger.log("d", "Cloud flow is possible!")
            self.cloudFlowIsPossible.emit()

    def _onCloudFlowPossible(self) -> None:
        # Cloud flow is possible, so show the message
        if not self._start_cloud_flow_message:
            self._createCloudFlowStartMessage()
        if self._start_cloud_flow_message and not self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.show()

    def _onCloudPrintingConfigured(self, device) -> None:
        # Hide the cloud flow start message if it was hanging around already
        # For example: if the user already had the browser openen and made the association themselves
        if self._start_cloud_flow_message and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()

        # Cloud flow is complete, so show the message
        if not self._cloud_flow_complete_message:
            self._createCloudFlowCompleteMessage()
        if self._cloud_flow_complete_message and not self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.show()

        # Set the machine's cloud flow as complete so we don't ask the user again and again for cloud connected printers
        active_machine = self._application.getMachineManager().activeMachine
        if active_machine:

            # The active machine _might_ not be the machine that was in the added cloud cluster and
            # then this will hide the cloud message for the wrong machine. So we only set it if the
            # host names match between the active machine and the newly added cluster
            saved_host_name = active_machine.getMetaDataEntry(
                "um_network_key", "").split('.')[0]
            added_host_name = device.toDict()["host_name"]

            if added_host_name == saved_host_name:
                active_machine.setMetaDataEntry("do_not_show_cloud_message",
                                                True)

        return

    def _onDontAskMeAgain(self, checked: bool) -> None:
        active_machine = self._application.getMachineManager(
        ).activeMachine  # type: Optional[GlobalStack]
        if active_machine:
            active_machine.setMetaDataEntry("do_not_show_cloud_message",
                                            checked)
            if checked:
                Logger.log(
                    "d",
                    "Will not ask the user again to cloud connect for current printer."
                )
        return

    def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager(
        ).activeMachineAddress  # type: str
        if address:
            QDesktopServices.openUrl(
                QUrl("http://" + address + "/cloud_connect"))
            if self._start_cloud_flow_message:
                self._start_cloud_flow_message.hide()
                self._start_cloud_flow_message = None
        return

    def _onReviewCloudConnection(self, messageId: str, actionId: str) -> None:
        address = self._application.getMachineManager(
        ).activeMachineAddress  # type: str
        if address:
            QDesktopServices.openUrl(QUrl("http://" + address + "/settings"))
        return

    def _onMachineSwitched(self) -> None:
        # Hide any left over messages
        if self._start_cloud_flow_message is not None and self._start_cloud_flow_message.visible:
            self._start_cloud_flow_message.hide()
        if self._cloud_flow_complete_message is not None and self._cloud_flow_complete_message.visible:
            self._cloud_flow_complete_message.hide()

        # Check for cloud flow again with newly selected machine
        self.checkCloudFlowIsPossible(None)

    def _createCloudFlowStartMessage(self):
        self._start_cloud_flow_message = Message(
            text=i18n_catalog.i18nc(
                "@info:status",
                "Send and monitor print jobs from anywhere using your Ultimaker account."
            ),
            lifetime=0,
            image_source=QUrl.fromLocalFile(
                os.path.join(
                    PluginRegistry.getInstance().getPluginPath(
                        "UM3NetworkPrinting"), "resources", "svg",
                    "cloud-flow-start.svg")),
            image_caption=i18n_catalog.i18nc(
                "@info:status Ultimaker Cloud is a brand name and shouldn't be translated.",
                "Connect to Ultimaker Cloud"),
            option_text=i18n_catalog.i18nc(
                "@action", "Don't ask me again for this printer."),
            option_state=False)
        self._start_cloud_flow_message.addAction(
            "", i18n_catalog.i18nc("@action", "Get started"), "", "")
        self._start_cloud_flow_message.optionToggled.connect(
            self._onDontAskMeAgain)
        self._start_cloud_flow_message.actionTriggered.connect(
            self._onCloudFlowStarted)

    def _createCloudFlowCompleteMessage(self):
        self._cloud_flow_complete_message = Message(
            text=i18n_catalog.i18nc(
                "@info:status",
                "You can now send and monitor print jobs from anywhere using your Ultimaker account."
            ),
            lifetime=30,
            image_source=QUrl.fromLocalFile(
                os.path.join(
                    PluginRegistry.getInstance().getPluginPath(
                        "UM3NetworkPrinting"), "resources", "svg",
                    "cloud-flow-completed.svg")),
            image_caption=i18n_catalog.i18nc("@info:status", "Connected!"))
        self._cloud_flow_complete_message.addAction(
            "", i18n_catalog.i18nc("@action", "Review your connection"), "",
            "", 1)  # TODO: Icon
        self._cloud_flow_complete_message.actionTriggered.connect(
            self._onReviewCloudConnection)
class DiscoverOctoPrintAction(MachineAction):
    def __init__(self, parent=None):
        super().__init__("DiscoverOctoPrintAction",
                         catalog.i18nc("@action", "Connect OctoPrint"))

        self._qml_url = "DiscoverOctoPrintAction.qml"
        self._window = None
        self._context = None

        self._network_plugin = None

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._manager = QNetworkAccessManager()
        self._manager.finished.connect(self._onRequestFinished)

        self._settings_reply = None

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            plugin_version = "Unknown"
            Logger.logException(
                "w", "Could not get version information for the plugin")

        self._user_agent = (
            "%s/%s %s/%s" %
            (Application.getInstance().getApplicationName(),
             Application.getInstance().getVersion(), "OctoPrintPlugin",
             Application.getInstance().getVersion())).encode()

        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False

        self._additional_components = None

        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerAdded)
        Application.getInstance().engineCreatedSignal.connect(
            self._createAdditionalComponentsView)

    @pyqtSlot()
    def startDiscovery(self):
        if not self._network_plugin:
            self._network_plugin = Application.getInstance(
            ).getOutputDeviceManager().getOutputDevicePlugin("OctoPrintPlugin")
            self._network_plugin.addInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.removeInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.instanceListChanged.connect(
                self._onInstanceDiscovery)
            self.instancesChanged.emit()
        else:
            # Restart bonjour discovery
            self._network_plugin.startDiscovery()

    def _onInstanceDiscovery(self, *args):
        self.instancesChanged.emit()

    @pyqtSlot(str)
    def removeManualInstance(self, name):
        if not self._network_plugin:
            return

        self._network_plugin.removeManualInstance(name)

    @pyqtSlot(str, str, int, str, bool, str, str)
    def setManualInstance(self, name, address, port, path, useHttps, userName,
                          password):
        # This manual printer could replace a current manual printer
        self._network_plugin.removeManualInstance(name)

        self._network_plugin.addManualInstance(name, address, port, path,
                                               useHttps, userName, password)

    def _onContainerAdded(self, container):
        # Add this action as a supported action to all machine definitions
        if isinstance(container,
                      DefinitionContainer) and container.getMetaDataEntry(
                          "type") == "machine" and container.getMetaDataEntry(
                              "supports_usb_connection"):
            Application.getInstance().getMachineActionManager(
            ).addSupportedAction(container.getId(), self.getKey())

    instancesChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=instancesChanged)
    def discoveredInstances(self):
        if self._network_plugin:
            instances = list(self._network_plugin.getInstances().values())
            instances.sort(key=lambda k: k.name)
            return instances
        else:
            return []

    @pyqtSlot(str)
    def setKey(self, key):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if "octoprint_id" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry("octoprint_id", key)
            else:
                global_container_stack.addMetaDataEntry("octoprint_id", key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    @pyqtSlot(result=str)
    def getStoredKey(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            meta_data = global_container_stack.getMetaData()
            if "octoprint_id" in meta_data:
                return global_container_stack.getMetaDataEntry("octoprint_id")

        return ""

    @pyqtSlot(str, str, str, str)
    def testApiKey(self,
                   base_url,
                   api_key,
                   basic_auth_username="",
                   basic_auth_password=""):
        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self.selectedInstanceSettingsChanged.emit()

        if api_key != "":
            Logger.log(
                "d",
                "Trying to access OctoPrint instance at %s with the provided API key."
                % base_url)

            ## Request 'settings' dump
            url = QUrl(base_url + "api/settings")
            settings_request = QNetworkRequest(url)
            settings_request.setRawHeader("X-Api-Key".encode(),
                                          api_key.encode())
            settings_request.setRawHeader("User-Agent".encode(),
                                          self._user_agent)
            if basic_auth_username and basic_auth_password:
                data = base64.b64encode(
                    ("%s:%s" % (basic_auth_username,
                                basic_auth_password)).encode()).decode("utf-8")
                settings_request.setRawHeader("Authorization".encode(),
                                              ("Basic %s" % data).encode())
            self._settings_reply = self._manager.get(settings_request)
        else:
            if self._settings_reply:
                self._settings_reply.abort()
                self._settings_reply = None

    @pyqtSlot(str)
    def setApiKey(self, api_key):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            if "octoprint_api_key" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry(
                    "octoprint_api_key", api_key)
            else:
                global_container_stack.addMetaDataEntry(
                    "octoprint_api_key", api_key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    apiKeyChanged = pyqtSignal()

    ##  Get the stored API key of this machine
    #   \return key String containing the key of the machine.
    @pyqtProperty(str, notify=apiKeyChanged)
    def apiKey(self):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack:
            return global_container_stack.getMetaDataEntry("octoprint_api_key")
        else:
            return ""

    selectedInstanceSettingsChanged = pyqtSignal()

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceResponded(self):
        return self._instance_responded

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceApiKeyAccepted(self):
        return self._instance_api_key_accepted

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsSd(self):
        return self._instance_supports_sd

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsCamera(self):
        return self._instance_supports_camera

    @pyqtSlot(str, str, str)
    def setContainerMetaDataEntry(self, container_id, key, value):
        containers = ContainerRegistry.getInstance().findContainers(
            id=container_id)
        if not containers:
            UM.Logger.log(
                "w",
                "Could not set metadata of container %s because it was not found.",
                container_id)
            return False

        container = containers[0]
        if key in container.getMetaData():
            container.setMetaDataEntry(key, value)
        else:
            container.addMetaDataEntry(key, value)

    @pyqtSlot(bool)
    def applyGcodeFlavorFix(self, apply_fix):
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if not global_container_stack:
            return

        gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode"
        if global_container_stack.getProperty("machine_gcode_flavor",
                                              "value") == gcode_flavor:
            # No need to add a definition_changes container if the setting is not going to be changed
            return

        # Make sure there is a definition_changes container to store the machine settings
        definition_changes_container = global_container_stack.definitionChanges
        if definition_changes_container == ContainerRegistry.getInstance(
        ).getEmptyInstanceContainer():
            definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
                global_container_stack,
                global_container_stack.getId() + "_settings")

        definition_changes_container.setProperty("machine_gcode_flavor",
                                                 "value", gcode_flavor)

        # Update the has_materials metadata flag after switching gcode flavor
        definition = global_container_stack.getBottom()
        if definition.getProperty(
                "machine_gcode_flavor",
                "value") != "UltiGCode" or definition.getMetaDataEntry(
                    "has_materials", False):
            # In other words: only continue for the UM2 (extended), but not for the UM2+
            return

        has_materials = global_container_stack.getProperty(
            "machine_gcode_flavor", "value") != "UltiGCode"

        material_container = global_container_stack.material

        if has_materials:
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.setMetaDataEntry("has_materials", True)
            else:
                global_container_stack.addMetaDataEntry("has_materials", True)

            # Set the material container to a sane default
            if material_container == ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer():
                search_criteria = {
                    "type":
                    "material",
                    "definition":
                    "fdmprinter",
                    "id":
                    global_container_stack.getMetaDataEntry(
                        "preferred_material")
                }
                materials = ContainerRegistry.getInstance(
                ).findInstanceContainers(**search_criteria)
                if materials:
                    global_container_stack.material = materials[0]
        else:
            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.removeMetaDataEntry("has_materials")

            global_container_stack.material = ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer()

        Application.getInstance().globalContainerStackChanged.emit()

    @pyqtSlot(str)
    def openWebPage(self, url):
        QDesktopServices.openUrl(QUrl(url))

    def _createAdditionalComponentsView(self):
        Logger.log(
            "d",
            "Creating additional ui components for OctoPrint-connected printers."
        )

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
                            "OctoPrintComponents.qml")
        self._additional_components = Application.getInstance(
        ).createQmlComponent(path, {"manager": self})
        if not self._additional_components:
            Logger.log(
                "w",
                "Could not create additional components for OctoPrint-connected printers."
            )
            return

        Application.getInstance().addAdditionalComponent(
            "monitorButtons",
            self._additional_components.findChild(QObject,
                                                  "openOctoPrintButton"))

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply):

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                if http_status_code == 200:
                    Logger.log("d", "API key accepted by OctoPrint.")
                    self._instance_api_key_accepted = True

                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data[
                            "feature"]:
                        self._instance_supports_sd = json_data["feature"][
                            "sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data[
                            "webcam"]:
                        stream_url = json_data["webcam"]["streamUrl"]
                        if stream_url:  #not empty string or None
                            self._instance_supports_camera = True

                elif http_status_code == 401:
                    Logger.log("d", "Invalid API key for OctoPrint.")
                    self._instance_api_key_accepted = False

                self._instance_responded = True
                self.selectedInstanceSettingsChanged.emit()
Exemple #38
0
class Toolbox(QObject, Extension):
    def __init__(self, application: CuraApplication) -> None:
        super().__init__()

        self._application = application  # type: CuraApplication

        self._sdk_version = ApplicationMetadata.CuraSDKVersion  # type: Union[str, int]
        self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion  # type: str
        self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot  # type: str
        self._api_url = None  # type: Optional[str]

        # Network:
        self._download_request = None  # type: Optional[QNetworkRequest]
        self._download_reply = None  # type: Optional[QNetworkReply]
        self._download_progress = 0  # type: float
        self._is_downloading = False  # type: bool
        self._network_manager = None  # type: Optional[QNetworkAccessManager]
        self._request_headers = [] # type: List[Tuple[bytes, bytes]]
        self._updateRequestHeader()

        self._request_urls = {}  # type: Dict[str, QUrl]
        self._to_update = []  # type: List[str] # Package_ids that are waiting to be updated
        self._old_plugin_ids = set()  # type: Set[str]
        self._old_plugin_metadata = dict()  # type: Dict[str, Dict[str, Any]]

        # The responses as given by the server parsed to a list.
        self._server_response_data = {
            "authors":             [],
            "packages":            []
        }  # type: Dict[str, List[Any]]

        # Models:
        self._models = {
            "authors":             AuthorsModel(self),
            "packages":            PackagesModel(self),
        }  # type: Dict[str, Union[AuthorsModel, PackagesModel]]

        self._plugins_showcase_model = PackagesModel(self)
        self._plugins_available_model = PackagesModel(self)
        self._plugins_installed_model = PackagesModel(self)

        self._materials_showcase_model = AuthorsModel(self)
        self._materials_available_model = AuthorsModel(self)
        self._materials_installed_model = PackagesModel(self)
        self._materials_generic_model = PackagesModel(self)

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin"  # type: str

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "welcome"  # type: str

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None  # type: Optional[Dict[str, Any]]

        self._dialog = None  # type: Optional[QObject]
        self._confirm_reset_dialog = None  # type: Optional[QObject]
        self._resetUninstallVariables()

        self._restart_required = False  # type: bool

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = ""  # type: str
        self._license_dialog_license_content = ""  # type: str
        self._license_dialog_plugin_file_location = ""  # type: str
        self._restart_dialog_message = ""  # type: str

        self._application.initializationFinished.connect(self._onAppInitialized)
        self._application.getCuraAPI().account.accessTokenChanged.connect(self._updateRequestHeader)

    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()
    uninstallVariablesChanged = pyqtSignal()

    ##  Go back to the start state (welcome screen or loading if no login required)
    def _restart(self):
        self._updateRequestHeader()
        # For an Essentials build, login is mandatory
        if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion:
            self.setViewPage("welcome")
        else:
            self.setViewPage("loading")
            self._fetchPackageData()

    def _updateRequestHeader(self):
        self._request_headers = [
            (b"User-Agent",
            str.encode(
                "%s/%s (%s %s)" % (
                    self._application.getApplicationName(),
                    self._application.getVersion(),
                    platform.system(),
                    platform.machine(),
                )
            ))
        ]
        access_token = self._application.getCuraAPI().account.accessToken
        if access_token:
            self._request_headers.append((b"Authorization", "Bearer {}".format(access_token).encode()))

    def _resetUninstallVariables(self) -> None:
        self._package_id_to_uninstall = None  # type: Optional[str]
        self._package_name_to_uninstall = ""
        self._package_used_materials = []  # type: List[Tuple[GlobalStack, str, str]]
        self._package_used_qualities = []  # type: List[Tuple[GlobalStack, str, str]]

    @pyqtSlot(str, int)
    def ratePackage(self, package_id: str, rating: int) -> None:
        url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url=self._api_url, package_id = package_id))

        self._rate_request = QNetworkRequest(url)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest, self._rate_request).setRawHeader(header_name, header_value)
        data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating)
        self._rate_reply = cast(QNetworkAccessManager, self._network_manager).put(self._rate_request, data.encode())

    @pyqtSlot(result = str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result = str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result = str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._plugin_registry = self._application.getPluginRegistry()
        self._package_manager = self._application.getPackageManager()
        self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
            cloud_api_root = self._cloud_api_root,
            cloud_api_version = self._cloud_api_version,
            sdk_version = self._sdk_version
        )
        self._request_urls = {
            "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
            "packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
        }

        self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)

        if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check"):
            # Request the latest and greatest!
            self._fetchPackageData()

    def _fetchPackageData(self):
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")

        # Gather installed packages:
        self._updateInstalledModels()

    # Displays the toolbox
    @pyqtSlot()
    def launch(self) -> None:

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")

        if not self._dialog:
            Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.")
            return

        self._restart()

        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name)
        plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
        if not plugin_path:
            return None
        path = os.path.join(plugin_path, "resources", "qml", qml_name)
        
        dialog = self._application.createQmlComponent(path, {"toolbox": self})
        if not dialog:
            raise Exception("Failed to create Marketplace dialog")
        return dialog

    def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
        try:
            highest_sdk_version_supported = Version(0)
            for supported_version in plugin_data["plugin"]["supported_sdk_versions"]:
                if supported_version > highest_sdk_version_supported:
                    highest_sdk_version_supported = supported_version

            formatted = {
                "package_id": plugin_data["id"],
                "package_type": "plugin",
                "display_name": plugin_data["plugin"]["name"],
                "package_version": plugin_data["plugin"]["version"],
                "sdk_version": highest_sdk_version_supported,
                "author": {
                    "author_id": plugin_data["plugin"]["author"],
                    "display_name": plugin_data["plugin"]["author"]
                },
                "is_installed": True,
                "description": plugin_data["plugin"]["description"]
            }
            return formatted
        except KeyError:
            Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
            return None

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        # This is moved here to avoid code duplication and so that after installing plugins they get removed from the
        # list of old plugins
        old_plugin_ids = self._plugin_registry.getInstalledPlugins()
        installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
        scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()

        self._old_plugin_ids = set()
        self._old_plugin_metadata = dict()

        for plugin_id in old_plugin_ids:
            # Neither the installed packages nor the packages that are scheduled to remove are old plugins
            if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
                Logger.log("d", "Found a plugin that was installed with the old plugin browser: %s", plugin_id)

                old_metadata = self._plugin_registry.getMetaData(plugin_id)
                new_metadata = self._convertPluginMetadata(old_metadata)
                if new_metadata is None:
                    # Something went wrong converting it.
                    continue
                self._old_plugin_ids.add(plugin_id)
                self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata

        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
            # The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
            # plugins should be excluded from the old plugins list/dict.
            all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
            self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
                                    if plugin_id not in all_plugin_package_ids)
            self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}

            self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._materials_installed_model.setMetadata(all_packages["material"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Check package usage and uninstall
    #   If the package is in use, you'll get a confirmation dialog to set everything to default
    @pyqtSlot(str)
    def checkPackageUsageAndUninstall(self, package_id: str) -> None:
        package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id)
        if package_used_materials or package_used_qualities:
            # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
            self._package_id_to_uninstall = package_id
            package_info = self._package_manager.getInstalledPackageInfo(package_id)
            self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id"))
            self._package_used_materials = package_used_materials
            self._package_used_qualities = package_used_qualities
            # Ask change to default material / profile
            if self._confirm_reset_dialog is None:
                self._confirm_reset_dialog = self._createDialog("dialogs/ToolboxConfirmUninstallResetDialog.qml")
            self.uninstallVariablesChanged.emit()
            if self._confirm_reset_dialog is None:
                Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.")
            else:
                self._confirm_reset_dialog.show()
        else:
            # Plain uninstall
            self.uninstall(package_id)

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def pluginToUninstall(self) -> str:
        return self._package_name_to_uninstall

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedMaterials(self) -> str:
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])

    @pyqtProperty(str, notify = uninstallVariablesChanged)
    def uninstallUsedQualities(self) -> str:
        return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])

    @pyqtSlot()
    def closeConfirmResetDialog(self) -> None:
        if self._confirm_reset_dialog is not None:
            self._confirm_reset_dialog.close()

    ##  Uses "uninstall variables" to reset qualities and materials, then uninstall
    #   It's used as an action on Confirm reset on Uninstall
    @pyqtSlot()
    def resetMaterialsQualitiesAndUninstall(self) -> None:
        application = CuraApplication.getInstance()
        machine_manager = application.getMachineManager()
        container_tree = ContainerTree.getInstance()

        for global_stack, extruder_nr, container_id in self._package_used_materials:
            extruder = global_stack.extruderList[int(extruder_nr)]
            approximate_diameter = extruder.getApproximateMaterialDiameter()
            variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()]
            default_material_node = variant_node.preferredMaterial(approximate_diameter)
            machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack)
        for global_stack, extruder_nr, container_id in self._package_used_qualities:
            variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
            material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
            extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
            definition_id = global_stack.definition.getId()
            machine_node = container_tree.machines[definition_id]
            default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type]
            machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)

        if self._package_id_to_uninstall is not None:
            self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
            self.uninstall(self._package_id_to_uninstall)
        self._resetUninstallVariables()
        self.closeConfirmResetDialog()

    def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
        container_registry = self._application.getContainerRegistry()

        all_containers = self._package_manager.getPackageContainerIds(package_id)
        for container_id in all_containers:
            containers = container_registry.findInstanceContainers(id = container_id)
            if not containers:
                continue
            container = containers[0]
            if container.getMetaDataEntry("type") != "material":
                continue
            root_material_id = container.getMetaDataEntry("base_file")
            root_material_containers = container_registry.findInstanceContainers(id = root_material_id)
            if not root_material_containers:
                continue
            root_material_container = root_material_containers[0]
            root_material_container.setMetaDataEntry("removed", True)

    @pyqtSlot(str)
    def uninstall(self, package_id: str) -> None:
        self._package_manager.removePackage(package_id, force_add = True)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Actual update packages that are in self._to_update
    def _update(self) -> None:
        if self._to_update:
            plugin_id = self._to_update.pop(0)
            remote_package = self.getRemotePackage(plugin_id)
            if remote_package:
                download_url = remote_package["download_url"]
                Logger.log("d", "Updating package [%s]..." % plugin_id)
                self.startDownload(download_url)
            else:
                Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id)

        if self._to_update:
            self._application.callLater(self._update)

    ##  Update a plugin by plugin_id
    @pyqtSlot(str)
    def update(self, plugin_id: str) -> None:
        self._to_update.append(plugin_id)
        self._application.callLater(self._update)

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify = metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify = restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self) -> None:
        self._application.windowClosed()

    def getRemotePackage(self, package_id: str) -> Optional[Dict]:
        # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
        remote_package = None
        for package in self._server_response_data["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
                break
        return remote_package

    @pyqtSlot(str, result = bool)
    def canDowngrade(self, package_id: str) -> bool:
        # If the currently installed version is higher than the bundled version (if present), the we can downgrade
        # this package.
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        bundled_package = self._package_manager.getBundledPackageInfo(package_id)
        if bundled_package is None:
            return False

        local_version = Version(local_package["package_version"])
        bundled_version = Version(bundled_package["package_version"])
        return bundled_version < local_version

    @pyqtSlot(str, result = bool)
    def isInstalled(self, package_id: str) -> bool:
        result = self._package_manager.isPackageInstalled(package_id)
        # Also check the old plugins list if it's not found in the package manager.
        if not result:
            result = self.isOldPlugin(package_id)
        return result

    @pyqtSlot(str, result = int)
    def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._materials_installed_model.items:
            if package["author_id"] == author_id:
                count += 1
        return count

    # This slot is only used to get the number of material packages by author, not any other type of packages.
    @pyqtSlot(str, result = int)
    def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
        count = 0
        for package in self._server_response_data["packages"]:
            if package["package_type"] == "material":
                if package["author"]["author_id"] == author_id:
                    count += 1
        return count

    @pyqtSlot(str, result = bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    # Check for plugins that were installed with the old plugin browser
    def isOldPlugin(self, plugin_id: str) -> bool:
        return plugin_id in self._old_plugin_ids

    def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
        return self._old_plugin_metadata.get(plugin_id)

    def isLoadingComplete(self) -> bool:
        populated = 0
        for metadata_list in self._server_response_data.items():
            if metadata_list:
                populated += 1
        return populated == len(self._server_response_data.items())

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, request_type: str) -> None:
        Logger.log("d", "Requesting %s metadata from server.", request_type)
        request = QNetworkRequest(self._request_urls[request_type])
        for header_name, header_value in self._request_headers:
            request.setRawHeader(header_name, header_value)
        if self._network_manager:
            self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Attempting to download & install package from %s.", url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
        for header_name, header_value in self._request_headers:
            cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value)
        self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "User cancelled the download of a package.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            try:
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
            except (TypeError, RuntimeError):  # Raised when the method is not connected to the signal yet.
                pass  # Don't need to disconnect.
            try:
                self._download_reply.abort()
            except RuntimeError:
                # In some cases the garbage collector is a bit to agressive, which causes the dowload_reply
                # to be deleted (especially if the machine has been put to sleep). As we don't know what exactly causes
                # this (The issue probably lives in the bowels of (py)Qt somewhere), we can only catch and ignore it.
                pass
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(self, network_accessibility: QNetworkAccessManager.NetworkAccessibility) -> None:
        if network_accessibility == QNetworkAccessManager.NotAccessible:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for response_type, url in self._request_urls.items():
                if reply.url() == url:
                    if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
                        try:
                            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                            # Check for errors:
                            if "errors" in json_data:
                                for error in json_data["errors"]:
                                    Logger.log("e", "%s", error["title"])
                                return

                            # Create model and apply metadata:
                            if not self._models[response_type]:
                                Logger.log("e", "Could not find the %s model.", response_type)
                                break
                            
                            self._server_response_data[response_type] = json_data["data"]
                            self._models[response_type].setMetadata(self._server_response_data[response_type])

                            if response_type == "packages":
                                self._models[response_type].setFilter({"type": "plugin"})
                                self.reBuildMaterialsModels()
                                self.reBuildPluginsModels()
                                self._notifyPackageManager()
                            elif response_type == "authors":
                                self._models[response_type].setFilter({"package_types": "material"})
                                self._models[response_type].setFilter({"tags": "generic"})

                            self.metadataChanged.emit()

                            if self.isLoadingComplete():
                                self.setViewPage("overview")

                        except json.decoder.JSONDecodeError:
                            Logger.log("w", "Received invalid JSON for %s.", response_type)
                            break
                    else:
                        Logger.log("w", "Unable to connect with the server, we got a response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
                        self.setViewPage("errored")
                        self.resetDownload()
        elif reply.operation() == QNetworkAccessManager.PutOperation:
            # Ignore any operation that is not a get operation
            pass

    # This function goes through all known remote versions of a package and notifies the package manager of this change
    def _notifyPackageManager(self):
        for package in self._server_response_data["packages"]:
            self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                self._download_reply = cast(QNetworkReply, self._download_reply)
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
                
                # Check if the download was sucessfull
                if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                    try:
                        Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
                    except json.decoder.JSONDecodeError:
                        Logger.logException("w", "Failed to download package and failed to parse a response from it")
                    finally:
                        return
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str) -> None:
        Logger.log("i", "Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content, file_path)
            return

        self.install(file_path)

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: float) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
    def downloadProgress(self) -> float:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        if self._active_package != package:
            self._active_package = package
            self.activePackageChanged.emit()

    ##  The active package is the package that is currently being downloaded
    @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        if self._view_category != category:
            self._view_category = category
            self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        if self._view_page != page:
            self._view_page = page
            self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewPage, notify = viewChanged)
    def viewPage(self) -> str:
        return self._view_page

    # Exposed Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, constant=True)
    def authorsModel(self) -> AuthorsModel:
        return cast(AuthorsModel, self._models["authors"])

    @pyqtProperty(QObject, constant=True)
    def packagesModel(self) -> PackagesModel:
        return cast(PackagesModel, self._models["packages"])

    @pyqtProperty(QObject, constant=True)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return self._plugins_showcase_model

    @pyqtProperty(QObject, constant=True)
    def pluginsAvailableModel(self) -> PackagesModel:
        return self._plugins_available_model

    @pyqtProperty(QObject, constant=True)
    def pluginsInstalledModel(self) -> PackagesModel:
        return self._plugins_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsShowcaseModel(self) -> AuthorsModel:
        return self._materials_showcase_model

    @pyqtProperty(QObject, constant=True)
    def materialsAvailableModel(self) -> AuthorsModel:
        return self._materials_available_model

    @pyqtProperty(QObject, constant=True)
    def materialsInstalledModel(self) -> PackagesModel:
        return self._materials_installed_model

    @pyqtProperty(QObject, constant=True)
    def materialsGenericModel(self) -> PackagesModel:
        return self._materials_generic_model

    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({filter_type: parameter})
        self.filterChanged.emit()

    @pyqtSlot(str, "QVariantMap")
    def setFilters(self, model_type: str, filter_dict: dict) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter(filter_dict)
        self.filterChanged.emit()

    @pyqtSlot(str)
    def removeFilters(self, model_type: str) -> None:
        if not self._models[model_type]:
            Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
            return
        self._models[model_type].setFilter({})
        self.filterChanged.emit()

    # HACK(S):
    # --------------------------------------------------------------------------
    def reBuildMaterialsModels(self) -> None:
        materials_showcase_metadata = []
        materials_available_metadata = []
        materials_generic_metadata = []

        processed_authors = []  # type: List[str]

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "material":

                author = item["author"]
                if author["author_id"] in processed_authors:
                    continue

                # Generic materials to be in the same section
                if "generic" in item["tags"]:
                    materials_generic_metadata.append(item)
                else:
                    if "showcase" in item["tags"]:
                        materials_showcase_metadata.append(author)
                    else:
                        materials_available_metadata.append(author)

                    processed_authors.append(author["author_id"])

        self._materials_showcase_model.setMetadata(materials_showcase_metadata)
        self._materials_available_model.setMetadata(materials_available_metadata)
        self._materials_generic_model.setMetadata(materials_generic_metadata)

    def reBuildPluginsModels(self) -> None:
        plugins_showcase_metadata = []
        plugins_available_metadata = []

        for item in self._server_response_data["packages"]:
            if item["package_type"] == "plugin":
                if "showcase" in item["tags"]:
                    plugins_showcase_metadata.append(item)
                else:
                    plugins_available_metadata.append(item)

        self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
        self._plugins_available_model.setMetadata(plugins_available_metadata)
Exemple #39
0
    def __init__(self, site, default_path):
        QWidget.__init__(self)

        self.site = site
        if default_path:
            self.default_path = default_path
        #else:
        #    self.default_path = os.path.join(Generator.install_directory, "sources")

        vbox = QVBoxLayout()
        layout = QGridLayout()
        title = QLabel()
        title.setText("Dashboard")
        fnt = title.font()
        fnt.setPointSize(20)
        fnt.setBold(True)
        title.setFont(fnt)

        self.browser = QTextBrowser()
        self.browser.setOpenLinks(False)

        self.load_button = FlatButton(":/images/load_normal.png",
                                      ":/images/load_hover.png",
                                      ":/images/load_pressed.png")
        self.load_button.setToolTip("Load an existing app project")

        self.create_button = FlatButton(":/images/create_normal.png",
                                        ":/images/create_hover.png",
                                        ":/images/create_pressed.png")
        self.create_button.setToolTip("Create a app project")

        self.publish_button = FlatButton(":/images/publish_normal.png",
                                         ":/images/publish_hover.png",
                                         ":/images/publish_pressed.png")
        self.publish_button.setToolTip(
            "Upload the app to your web space provider")

        self.preview_button = FlatButton(":/images/preview_normal.png",
                                         ":/images/preview_hover.png",
                                         ":/images/preview_pressed.png")
        self.preview_button.setToolTip("Load the app in your browser locally")

        self.build_button = FlatButton(":/images/build_normal.png",
                                       ":/images/build_hover.png",
                                       ":/images/build_pressed.png")
        self.build_button.setToolTip("Build the app")

        self.info = QLabel()
        if self.site and self.site.title:
            self.info.setText(self.site.title + " loaded...")
        else:
            self.info.setText("No app loaded yet...")

        space = QWidget()
        space2 = QWidget()
        space3 = QWidget()
        space.setMinimumHeight(30)
        space2.setMinimumHeight(30)
        space3.setMinimumHeight(30)
        layout.addWidget(title, 0, 0, 1, 3)
        layout.addWidget(self.info, 1, 0, 1, 3)
        layout.addWidget(space, 2, 0)
        layout.addWidget(self.load_button, 3, 0, 1, 1, Qt.AlignCenter)
        layout.addWidget(self.create_button, 3, 1, 1, 1, Qt.AlignCenter)
        layout.addWidget(self.publish_button, 3, 2, 1, 1, Qt.AlignCenter)
        layout.addWidget(space2, 4, 0)
        layout.addWidget(self.preview_button, 5, 0, 1, 1, Qt.AlignCenter)
        layout.addWidget(self.build_button, 5, 1, 1, 1, Qt.AlignCenter)

        #load generator plugins here

        #for plugin_name in Plugins.generatorPluginNames():

        #    p = Plugins.getGeneratorPlugin(plugin_name)
        #    button = FlatButton(os.path.join(Plugins.getBundleDir(), "plugins",p.normal_image), os.path.join(Plugins.getBundleDir(), "plugins",p.hover_image), os.path.join(Plugins.getBundleDir(), "plugins",p.pressed_image))
        #    button.setToolTip("Build the website")
        #    button.clicked.connect(p.clicked)
        #    layout.addWidget(button, 5, 2, 1, 1, Qt.AlignCenter)

        vbox.addLayout(layout)
        vbox.addSpacing(40)
        vbox.addWidget(self.browser)
        self.setLayout(vbox)

        self.load_button.clicked.connect(self.loadClicked)
        self.create_button.clicked.connect(self.createClicked)
        self.publish_button.clicked.connect(self.publishClicked)
        self.preview_button.clicked.connect(self.previewClicked)
        self.build_button.clicked.connect(self.buildClicked)

        manager = QNetworkAccessManager(self)
        manager.finished.connect(self.fileIsReady)
class NetworkPrinterOutputDevicePlugin(OutputDevicePlugin):
    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._browser = None
        self._printers = {}

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"

        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        # List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
        # authentication requests.
        self._old_printers = []

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addPrinterSignal.connect(self.addPrinter)
        self.removePrinterSignal.connect(self.removePrinter)
        Application.getInstance().globalContainerStackChanged.connect(
            self.reCheckConnections)

        # Get list of manual printers from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference(
            "um3networkprinting/manual_instances",
            "")  #  A comma-separated list of ip adresses or hostnames
        self._manual_instances = self._preferences.getValue(
            "um3networkprinting/manual_instances").split(",")

    addPrinterSignal = Signal()
    removePrinterSignal = Signal()
    printerListChanged = Signal()

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._browser:
            self._browser.cancel()
            self._browser = None
            self._old_printers = [
                printer_name for printer_name in self._printers
            ]
            self._printers = {}
            self.printerListChanged.emit()
        # After network switching, one must make a new instance of Zeroconf
        # On windows, the instance creation is very fast (unnoticable). Other platforms?
        self._zero_conf = Zeroconf()
        self._browser = ServiceBrowser(self._zero_conf,
                                       u'_ultimaker._tcp.local.',
                                       [self._onServiceChanged])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualPrinter(address)

    def addManualPrinter(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

        name = address
        instance_name = "manual:%s" % address
        properties = {
            b"name": name.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true"
        }

        if instance_name not in self._printers:
            # Add a preliminary printer instance
            self.addPrinter(instance_name, address, properties)

        self.checkManualPrinter(address)

    def removeManualPrinter(self, key, address=None):
        if key in self._printers:
            if not address:
                address = self._printers[key].ipAddress
            self.removePrinter(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances",
                                       ",".join(self._manual_instances))

    def checkManualPrinter(self, address):
        # Check if a printer exists at this address
        # If a printer responds, it will replace the preliminary printer created above
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    ##  Handler for all requests that have finished.
    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()
        status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "system" in reply_url:  # Name returned from printer.
                if status_code == 200:
                    system_info = json.loads(
                        bytes(reply.readAll()).decode("utf-8"))
                    address = reply.url().host()
                    name = ("%s (%s)" % (system_info["name"], address))

                    instance_name = "manual:%s" % address
                    properties = {
                        b"name":
                        name.encode("utf-8"),
                        b"firmware_version":
                        system_info["firmware"].encode("utf-8"),
                        b"manual":
                        b"true"
                    }
                    if instance_name in self._printers:
                        # Only replace the printer if it is still in the list of (manual) printers
                        self.removePrinter(instance_name)
                        self.addPrinter(instance_name, address, properties)

    ##  Stop looking for devices on network.
    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def getPrinters(self):
        return self._printers

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        for key in self._printers:
            if key == active_machine.getMetaDataEntry("um_network_key"):
                Logger.log("d", "Connecting [%s]..." % key)
                self._printers[key].connect()
                self._printers[key].connectionStateChanged.connect(
                    self._onPrinterConnectionStateChanged)
            else:
                if self._printers[key].isConnected():
                    Logger.log("d", "Closing connection [%s]..." % key)
                    self._printers[key].close()

    ##  Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
    def addPrinter(self, name, address, properties):
        printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(
            name, address, properties, self._api_prefix)
        self._printers[printer.getKey()] = printer
        global_container_stack = Application.getInstance(
        ).getGlobalContainerStack()
        if global_container_stack and printer.getKey(
        ) == global_container_stack.getMetaDataEntry("um_network_key"):
            if printer.getKey(
            ) not in self._old_printers:  # Was the printer already connected, but a re-scan forced?
                Logger.log("d",
                           "addPrinter, connecting [%s]..." % printer.getKey())
                self._printers[printer.getKey()].connect()
                printer.connectionStateChanged.connect(
                    self._onPrinterConnectionStateChanged)
        self.printerListChanged.emit()

    def removePrinter(self, name):
        printer = self._printers.pop(name, None)
        if printer:
            if printer.isConnected():
                printer.connectionStateChanged.disconnect(
                    self._onPrinterConnectionStateChanged)
                Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
                printer.disconnect()
        self.printerListChanged.emit()

    ##  Handler for when the connection state of one of the detected printers changes
    def _onPrinterConnectionStateChanged(self, key):
        if key not in self._printers:
            return
        if self._printers[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._printers[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    ##  Handler for zeroConf detection
    def _onServiceChanged(self, zeroconf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zeroconf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zeroconf.cache.entries_with_name(name.lower()):
                info.update_record(zeroconf, time.time(), record)

            for record in zeroconf.cache.entries_with_name(info.server):
                info.update_record(zeroconf, time.time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zeroconf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type",
                                                     None).decode("utf-8")
                if type_of_device == "printer":
                    address = '.'.join(map(lambda n: str(n), info.address))
                    self.addPrinterSignal.emit(str(name), address,
                                               info.properties)
                else:
                    Logger.log(
                        "w",
                        "The type of the found device is '%s', not 'printer'! Ignoring.."
                        % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removePrinterSignal.emit(str(name))
Exemple #41
0
class Toolbox(QObject, Extension):
    DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str
    DEFAULT_CLOUD_API_VERSION = 1 #type: int

    def __init__(self, application: CuraApplication) -> None:
        super().__init__()

        self._application = application #type: CuraApplication

        self._sdk_version = None    # type: Optional[int]
        self._cloud_api_version = None # type: Optional[int]
        self._cloud_api_root = None # type: Optional[str]
        self._api_url = None    # type: Optional[str]

        # Network:
        self._download_request = None #type: Optional[QNetworkRequest]
        self._download_reply = None #type: Optional[QNetworkReply]
        self._download_progress = 0 #type: float
        self._is_downloading = False #type: bool
        self._network_manager = None #type: Optional[QNetworkAccessManager]
        self._request_header = [
            b"User-Agent",
            str.encode(
                "%s/%s (%s %s)" % (
                    self._application.getApplicationName(),
                    self._application.getVersion(),
                    platform.system(),
                    platform.machine(),
                )
            )
        ]
        self._request_urls = {} # type: Dict[str, QUrl]
        self._to_update = []  # type: List[str] # Package_ids that are waiting to be updated
        self._old_plugin_ids = [] # type: List[str]

        # Data:
        self._metadata = {
            "authors":             [],
            "packages":            [],
            "plugins_showcase":    [],
            "plugins_available":   [],
            "plugins_installed":   [],
            "materials_showcase":  [],
            "materials_available": [],
            "materials_installed": []
        } # type: Dict[str, List[Any]]

        # Models:
        self._models = {
            "authors":             AuthorsModel(self),
            "packages":            PackagesModel(self),
            "plugins_showcase":    PackagesModel(self),
            "plugins_available":   PackagesModel(self),
            "plugins_installed":   PackagesModel(self),
            "materials_showcase":  AuthorsModel(self),
            "materials_available": PackagesModel(self),
            "materials_installed": PackagesModel(self)
        } # type: Dict[str, ListModel]

        # These properties are for keeping track of the UI state:
        # ----------------------------------------------------------------------
        # View category defines which filter to use, and therefore effectively
        # which category is currently being displayed. For example, possible
        # values include "plugin" or "material", but also "installed".
        self._view_category = "plugin" #type: str

        # View page defines which type of page layout to use. For example,
        # possible values include "overview", "detail" or "author".
        self._view_page = "loading" #type: str

        # Active package refers to which package is currently being downloaded,
        # installed, or otherwise modified.
        self._active_package = None # type: Optional[Dict[str, Any]]

        self._dialog = None #type: Optional[QObject]
        self._restart_required = False #type: bool

        # variables for the license agreement dialog
        self._license_dialog_plugin_name = "" #type: str
        self._license_dialog_license_content = "" #type: str
        self._license_dialog_plugin_file_location = "" #type: str
        self._restart_dialog_message = "" #type: str

        self._application.initializationFinished.connect(self._onAppInitialized)



    # Signals:
    # --------------------------------------------------------------------------
    # Downloading changes
    activePackageChanged = pyqtSignal()
    onDownloadProgressChanged = pyqtSignal()
    onIsDownloadingChanged = pyqtSignal()
    restartRequiredChanged = pyqtSignal()
    installChanged = pyqtSignal()
    enabledChanged = pyqtSignal()

    # UI changes
    viewChanged = pyqtSignal()
    detailViewChanged = pyqtSignal()
    filterChanged = pyqtSignal()
    metadataChanged = pyqtSignal()
    showLicenseDialog = pyqtSignal()

    @pyqtSlot(result = str)
    def getLicenseDialogPluginName(self) -> str:
        return self._license_dialog_plugin_name

    @pyqtSlot(result = str)
    def getLicenseDialogPluginFileLocation(self) -> str:
        return self._license_dialog_plugin_file_location

    @pyqtSlot(result = str)
    def getLicenseDialogLicenseContent(self) -> str:
        return self._license_dialog_license_content

    def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
        self._license_dialog_plugin_name = plugin_name
        self._license_dialog_license_content = license_content
        self._license_dialog_plugin_file_location = plugin_file_location
        self.showLicenseDialog.emit()

    # This is a plugin, so most of the components required are not ready when
    # this is initialized. Therefore, we wait until the application is ready.
    def _onAppInitialized(self) -> None:
        self._plugin_registry = self._application.getPluginRegistry()
        self._package_manager = self._application.getPackageManager()
        self._sdk_version = self._getSDKVersion()
        self._cloud_api_version = self._getCloudAPIVersion()
        self._cloud_api_root = self._getCloudAPIRoot()
        self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
            cloud_api_root=self._cloud_api_root,
            cloud_api_version=self._cloud_api_version,
            sdk_version=self._sdk_version
        )
        self._request_urls = {
            "authors": QUrl("{base_url}/authors".format(base_url=self._api_url)),
            "packages": QUrl("{base_url}/packages".format(base_url=self._api_url)),
            "plugins_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
            "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url=self._api_url)),
            "materials_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)),
            "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url=self._api_url))
        }

    # Get the API root for the packages API depending on Cura version settings.
    def _getCloudAPIRoot(self) -> str:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_ROOT
        if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
            return self.DEFAULT_CLOUD_API_ROOT
        return cura.CuraVersion.CuraCloudAPIRoot # type: ignore

    # Get the cloud API version from CuraVersion
    def _getCloudAPIVersion(self) -> int:
        if not hasattr(cura, "CuraVersion"):
            return self.DEFAULT_CLOUD_API_VERSION
        if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
            return self.DEFAULT_CLOUD_API_VERSION
        return cura.CuraVersion.CuraCloudAPIVersion # type: ignore

    # Get the packages version depending on Cura version settings.
    def _getSDKVersion(self) -> int:
        if not hasattr(cura, "CuraVersion"):
            return self._plugin_registry.APIVersion
        if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
            return self._plugin_registry.APIVersion
        if not cura.CuraVersion.CuraSDKVersion: # type: ignore
            return self._plugin_registry.APIVersion
        return cura.CuraVersion.CuraSDKVersion # type: ignore

    @pyqtSlot()
    def browsePackages(self) -> None:
        # Create the network manager:
        # This was formerly its own function but really had no reason to be as
        # it was never called more than once ever.
        if self._network_manager is not None:
            self._network_manager.finished.disconnect(self._onRequestFinished)
            self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)
        self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)

        # Make remote requests:
        self._makeRequestByType("packages")
        self._makeRequestByType("authors")
        self._makeRequestByType("plugins_showcase")
        self._makeRequestByType("materials_showcase")

        # Gather installed packages:
        self._updateInstalledModels()

        if not self._dialog:
            self._dialog = self._createDialog("Toolbox.qml")
        self._dialog.show()

        # Apply enabled/disabled state to installed plugins
        self.enabledChanged.emit()

    def _createDialog(self, qml_name: str) -> Optional[QObject]:
        Logger.log("d", "Toolbox: Creating dialog [%s].", qml_name)
        path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name)
        dialog = self._application.createQmlComponent(path, {"toolbox": self})
        return dialog


    def _convertPluginMetadata(self, plugin: Dict[str, Any]) -> Dict[str, Any]:
        formatted = {
            "package_id": plugin["id"],
            "package_type": "plugin",
            "display_name": plugin["plugin"]["name"],
            "package_version": plugin["plugin"]["version"],
            "sdk_version": plugin["plugin"]["api"],
            "author": {
                "author_id": plugin["plugin"]["author"],
                "display_name": plugin["plugin"]["author"]
            },
            "is_installed": True,
            "description": plugin["plugin"]["description"]
        }
        return formatted

    @pyqtSlot()
    def _updateInstalledModels(self) -> None:
        # This is moved here to avoid code duplication and so that after installing plugins they get removed from the
        # list of old plugins
        old_plugin_ids = self._plugin_registry.getInstalledPlugins()
        installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
        scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()

        self._old_plugin_ids = []
        self._old_plugin_metadata = [] # type: List[Dict[str, Any]]

        for plugin_id in old_plugin_ids:
            # Neither the installed packages nor the packages that are scheduled to remove are old plugins
            if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
                Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id)

                old_metadata = self._plugin_registry.getMetaData(plugin_id)
                new_metadata = self._convertPluginMetadata(old_metadata)

                self._old_plugin_ids.append(plugin_id)
                self._old_plugin_metadata.append(new_metadata)

        all_packages = self._package_manager.getAllInstalledPackagesInfo()
        if "plugin" in all_packages:
            self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata
            self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
            self.metadataChanged.emit()
        if "material" in all_packages:
            self._metadata["materials_installed"] = all_packages["material"]
            # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE
            self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
            self.metadataChanged.emit()

    @pyqtSlot(str)
    def install(self, file_path: str) -> None:
        self._package_manager.installPackage(file_path)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def uninstall(self, plugin_id: str) -> None:
        self._package_manager.removePackage(plugin_id, force_add = True)
        self.installChanged.emit()
        self._updateInstalledModels()
        self.metadataChanged.emit()
        self._restart_required = True
        self.restartRequiredChanged.emit()

    ##  Actual update packages that are in self._to_update
    def _update(self) -> None:
        if self._to_update:
            plugin_id = self._to_update.pop(0)
            remote_package = self.getRemotePackage(plugin_id)
            if remote_package:
                download_url = remote_package["download_url"]
                Logger.log("d", "Updating package [%s]..." % plugin_id)
                self.startDownload(download_url)
            else:
                Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id)

        if self._to_update:
            self._application.callLater(self._update)

    ##  Update a plugin by plugin_id
    @pyqtSlot(str)
    def update(self, plugin_id: str) -> None:
        self._to_update.append(plugin_id)
        self._application.callLater(self._update)

    @pyqtSlot(str)
    def enable(self, plugin_id: str) -> None:
        self._plugin_registry.enablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'active'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtSlot(str)
    def disable(self, plugin_id: str) -> None:
        self._plugin_registry.disablePlugin(plugin_id)
        self.enabledChanged.emit()
        Logger.log("i", "%s was set as 'deactive'.", plugin_id)
        self._restart_required = True
        self.restartRequiredChanged.emit()

    @pyqtProperty(bool, notify = metadataChanged)
    def dataReady(self) -> bool:
        return self._packages_model is not None

    @pyqtProperty(bool, notify = restartRequiredChanged)
    def restartRequired(self) -> bool:
        return self._restart_required

    @pyqtSlot()
    def restart(self) -> None:
        self._application.windowClosed()

    def getRemotePackage(self, package_id: str) -> Optional[Dict]:
        # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
        remote_package = None
        for package in self._metadata["packages"]:
            if package["package_id"] == package_id:
                remote_package = package
                break
        return remote_package

    # Checks
    # --------------------------------------------------------------------------
    @pyqtSlot(str, result = bool)
    def canUpdate(self, package_id: str) -> bool:
        if self.isOldPlugin(package_id):
            return True

        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        remote_package = self.getRemotePackage(package_id)
        if remote_package is None:
            return False

        local_version = Version(local_package["package_version"])
        remote_version = Version(remote_package["package_version"])
        return remote_version > local_version

    @pyqtSlot(str, result = bool)
    def canDowngrade(self, package_id: str) -> bool:
        # If the currently installed version is higher than the bundled version (if present), the we can downgrade
        # this package.
        local_package = self._package_manager.getInstalledPackageInfo(package_id)
        if local_package is None:
            return False

        bundled_package = self._package_manager.getBundledPackageInfo(package_id)
        if bundled_package is None:
            return False

        local_version = Version(local_package["package_version"])
        bundled_version = Version(bundled_package["package_version"])
        return bundled_version < local_version

    @pyqtSlot(str, result = bool)
    def isInstalled(self, package_id: str) -> bool:
        return self._package_manager.isPackageInstalled(package_id)

    @pyqtSlot(str, result = bool)
    def isEnabled(self, package_id: str) -> bool:
        if package_id in self._plugin_registry.getActivePlugins():
            return True
        return False

    # Check for plugins that were installed with the old plugin browser
    @pyqtSlot(str, result = bool)
    def isOldPlugin(self, plugin_id: str) -> bool:
        if plugin_id in self._old_plugin_ids:
            return True
        return False

    def loadingComplete(self) -> bool:
        populated = 0
        for list in self._metadata.items():
            if len(list) > 0:
                populated += 1
        if populated == len(self._metadata.items()):
            return True
        return False

    # Make API Calls
    # --------------------------------------------------------------------------
    def _makeRequestByType(self, type: str) -> None:
        Logger.log("i", "Toolbox: Requesting %s metadata from server.", type)
        request = QNetworkRequest(self._request_urls[type])
        request.setRawHeader(*self._request_header)
        if self._network_manager:
            self._network_manager.get(request)

    @pyqtSlot(str)
    def startDownload(self, url: str) -> None:
        Logger.log("i", "Toolbox: Attempting to download & install package from %s.", url)
        url = QUrl(url)
        self._download_request = QNetworkRequest(url)
        if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
            # Patch for Qt 5.6-5.8
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
            # Patch for Qt 5.9+
            cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
        cast(QNetworkRequest, self._download_request).setRawHeader(*self._request_header)
        self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
        self.setDownloadProgress(0)
        self.setIsDownloading(True)
        cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)

    @pyqtSlot()
    def cancelDownload(self) -> None:
        Logger.log("i", "Toolbox: User cancelled the download of a plugin.")
        self.resetDownload()

    def resetDownload(self) -> None:
        if self._download_reply:
            try:
                self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
            except TypeError: #Raised when the method is not connected to the signal yet.
                pass #Don't need to disconnect.
            self._download_reply.abort()
        self._download_reply = None
        self._download_request = None
        self.setDownloadProgress(0)
        self.setIsDownloading(False)

    # Handlers for Network Events
    # --------------------------------------------------------------------------
    def _onNetworkAccessibleChanged(self, network_accessibility: QNetworkAccessManager.NetworkAccessibility) -> None:
        if network_accessibility == QNetworkAccessManager.NotAccessible:
            self.resetDownload()

    def _onRequestFinished(self, reply: QNetworkReply) -> None:
        if reply.error() == QNetworkReply.TimeoutError:
            Logger.log("w", "Got a timeout.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.error() == QNetworkReply.HostNotFoundError:
            Logger.log("w", "Unable to reach server.")
            self.setViewPage("errored")
            self.resetDownload()
            return

        if reply.operation() == QNetworkAccessManager.GetOperation:
            for type, url in self._request_urls.items():
                if reply.url() == url:
                    if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
                        try:
                            json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))

                            # Check for errors:
                            if "errors" in json_data:
                                for error in json_data["errors"]:
                                    Logger.log("e", "%s", error["title"])
                                return

                            # Create model and apply metadata:
                            if not self._models[type]:
                                Logger.log("e", "Could not find the %s model.", type)
                                break

                            # HACK: Eventually get rid of the code from here...
                            if type is "plugins_showcase" or type is "materials_showcase":
                                self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
                                self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
                                self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
                                self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
                            else:
                                # ...until here.
                                # This hack arises for multiple reasons but the main
                                # one is because there are not separate API calls
                                # for different kinds of showcases.
                                self._metadata[type] = json_data["data"]
                                self._models[type].setMetadata(self._metadata[type])

                            # Do some auto filtering
                            # TODO: Make multiple API calls in the future to handle this
                            if type is "packages":
                                self._models[type].setFilter({"type": "plugin"})
                            if type is "authors":
                                self._models[type].setFilter({"package_types": "material"})

                            self.metadataChanged.emit()

                            if self.loadingComplete() is True:
                                self.setViewPage("overview")

                            return
                        except json.decoder.JSONDecodeError:
                            Logger.log("w", "Toolbox: Received invalid JSON for %s.", type)
                            break
                    else:
                        self.setViewPage("errored")
                        self.resetDownload()
                        return

        else:
            # Ignore any operation that is not a get operation
            pass

    def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
        if bytes_total > 0:
            new_progress = bytes_sent / bytes_total * 100
            self.setDownloadProgress(new_progress)
            if bytes_sent == bytes_total:
                self.setIsDownloading(False)
                cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
                # Must not delete the temporary file on Windows
                self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
                file_path = self._temp_plugin_file.name
                # Write first and close, otherwise on Windows, it cannot read the file
                self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
                self._temp_plugin_file.close()
                self._onDownloadComplete(file_path)

    def _onDownloadComplete(self, file_path: str):
        Logger.log("i", "Toolbox: Download complete.")
        package_info = self._package_manager.getPackageInfo(file_path)
        if not package_info:
            Logger.log("w", "Toolbox: Package file [%s] was not a valid CuraPackage.", file_path)
            return

        license_content = self._package_manager.getPackageLicense(file_path)
        if license_content is not None:
            self.openLicenseDialog(package_info["package_id"], license_content, file_path)
            return

        self.install(file_path)
        return

    # Getter & Setters for Properties:
    # --------------------------------------------------------------------------
    def setDownloadProgress(self, progress: float) -> None:
        if progress != self._download_progress:
            self._download_progress = progress
            self.onDownloadProgressChanged.emit()

    @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
    def downloadProgress(self) -> float:
        return self._download_progress

    def setIsDownloading(self, is_downloading: bool) -> None:
        if self._is_downloading != is_downloading:
            self._is_downloading = is_downloading
            self.onIsDownloadingChanged.emit()

    @pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
    def isDownloading(self) -> bool:
        return self._is_downloading

    def setActivePackage(self, package: Dict[str, Any]) -> None:
        self._active_package = package
        self.activePackageChanged.emit()

    @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
    def activePackage(self) -> Optional[Dict[str, Any]]:
        return self._active_package

    def setViewCategory(self, category: str = "plugin") -> None:
        self._view_category = category
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
    def viewCategory(self) -> str:
        return self._view_category

    def setViewPage(self, page: str = "overview") -> None:
        self._view_page = page
        self.viewChanged.emit()

    @pyqtProperty(str, fset = setViewPage, notify = viewChanged)
    def viewPage(self) -> str:
        return self._view_page



    # Expose Models:
    # --------------------------------------------------------------------------
    @pyqtProperty(QObject, notify = metadataChanged)
    def authorsModel(self) -> AuthorsModel:
        return self._models["authors"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def packagesModel(self) -> PackagesModel:
        return self._models["packages"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsShowcaseModel(self) -> PackagesModel:
        return self._models["plugins_showcase"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def pluginsInstalledModel(self) -> PackagesModel:
        return self._models["plugins_installed"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsShowcaseModel(self) -> PackagesModel:
        return self._models["materials_showcase"]

    @pyqtProperty(QObject, notify = metadataChanged)
    def materialsInstalledModel(self) -> PackagesModel:
        return self._models["materials_installed"]



    # Filter Models:
    # --------------------------------------------------------------------------
    @pyqtSlot(str, str, str)
    def filterModelByProp(self, modelType: str, filterType: str, parameter: str):
        if not self._models[modelType]:
            Logger.log("w", "Toolbox: Couldn't filter %s model because it doesn't exist.", modelType)
            return
        self._models[modelType].setFilter({ filterType: parameter })
        self.filterChanged.emit()

    @pyqtSlot()
    def removeFilters(self, modelType: str):
        if not self._models[modelType]:
            Logger.log("w", "Toolbox: Couldn't remove filters on %s model because it doesn't exist.", modelType)
            return
        self._models[modelType].setFilter({})
        self.filterChanged.emit()
class updateV2ray(QObject):
    downloadFinish = pyqtSignal()

    def __init__(self,
                 v2rayapi=False,
                 url=False,
                 checkDownloadInfo=False,
                 downloadV2raycore=False):
        super().__init__()
        self.downloadURL = url
        self.reply = None
        self.outFile = None
        self.fileName = None
        self.httpRequestAborted = False
        self.v2rayAPI = v2rayapi
        self.v2raycoreAPIURL = QUrl(
            r"""https://api.github.com/repos/v2ray/v2ray-core/releases/latest"""
        )
        Qt.Everyday = 8
        self.downloadPath = False
        self.qnam = QNetworkAccessManager()
        self.startV2ray = False
        self.stopV2ray = False
        self.translate = QCoreApplication.translate
        self.msgBox = QMessageBox()
        self.fly = QApplication.desktop().screen().rect().center(
        ) - self.msgBox.rect().center()
        self.silentInstall = False
        self.installOption = "auto"
        if (self.v2rayAPI and self.downloadURL and checkDownloadInfo):
            self.getV2raycoreInfoFromGithub()
        elif (self.v2rayAPI and self.downloadURL and downloadV2raycore):
            self.downloadV2raycore()
        else:
            pass

    def getv2raycoreAPIURL(self):
        return self.v2raycoreAPIURL

    def enableSilentInstall(self):
        self.silentInstall = True

    def downloadV2raycore(self, url=False):
        if (url):
            self.downloadURL = url

        fileInfo = QFileInfo(self.downloadURL.path())
        self.fileName = fileName = fileInfo.fileName()

        if not fileName:
            fileName = "v2ray.zip"

        if QFile.exists(fileName):
            QFile.remove(fileName)

        self.outFile = QFile(fileName)

        if not self.outFile.open(QIODevice.WriteOnly):
            if (not self.silentInstall):
                self.msgBox.information(
                    QDialog().move(self.fly),
                    self.translate("updateV2ray",
                                   "Download {}").format(fileName),
                    self.translate("updateV2ray",
                                   "Unable to save the file {}: {}.").format(
                                       fileName, self.outFile.errorString()))
            self.outFile = None
            return

        self.httpRequestAborted = False

        if (not self.silentInstall):
            self.progressDialog = QProgressDialog()
            self.progressDialog.setLabelText(
                self.translate("updateV2ray", "v2ray-core is downloading..."))
            self.progressDialog.canceled.connect(self.cancelDownload)

        self.startRequest(self.downloadURL)

    def startRequest(self, url):
        self.reply = self.qnam.get(QNetworkRequest(url))
        self.reply.finished.connect(self.httpFinished)
        self.reply.readyRead.connect(self.httpReadyRead)
        if (not self.silentInstall):
            self.reply.downloadProgress.connect(self.updateDataReadProgress)

    def httpReadyRead(self):
        if self.outFile is not None:
            self.outFile.write(self.reply.readAll())

    def updateDataReadProgress(self, bytesRead, totalBytes):
        if self.httpRequestAborted: return

        self.progressDialog.setMaximum(totalBytes)
        self.progressDialog.setValue(bytesRead)

    def cancelDownload(self):
        self.httpRequestAborted = True
        if self.reply is not None:
            self.reply.abort()
            if QFile.exists(self.fileName):
                QFile.remove(self.fileName)

    def httpFinished(self):
        if self.httpRequestAborted:
            if self.outFile is not None:
                self.outFile.close()
                self.outFile.remove()
                del self.outFile

            self.reply.deleteLater()
            self.reply = None
            if not self.silentInstall: self.progressDialog.hide()
            return

        if not self.silentInstall: self.progressDialog.hide()
        self.outFile.flush()
        self.outFile.close()

        redirectionTarget = self.reply.attribute(
            QNetworkRequest.RedirectionTargetAttribute)

        if self.reply.error():
            self.outFile.remove()
            if not self.silentInstall:
                self.msgBox.information(
                    QDialog().move(self.fly),
                    self.translate("updateV2ray", "Download"),
                    self.translate("updateV2ray",
                                   "Download failed: {}.").format(
                                       self.reply.errorString()))
                self.progressDialog.close()

        elif redirectionTarget is not None:
            newUrl = self.downloadURL.resolved(redirectionTarget)
            self.downloadURL = newUrl
            self.reply.deleteLater()
            self.reply = None
            self.outFile.open(QIODevice.WriteOnly)
            self.outFile.resize(0)
            self.startRequest(self.downloadURL)
            return
        else:
            self.reply.deleteLater()
            self.reply = None
            self.outFile = None
            self.downloadFinish.emit()

    def getV2raycoreInfoFromGithub(self, url=False):
        if (url):
            self.downloadURL = url
        self.reqV2raycore = QNetworkRequest(self.downloadURL)
        self.qnam.finished.connect(self.handleResponse)
        self.qnam.get(self.reqV2raycore)

    def handleResponse(self, reply):
        """
        Read all API from https://api.github.com/repos/v2ray/v2ray-core/releases/latest
        """
        errorCode = reply.error()
        if (self.v2rayAPI):
            if errorCode == QNetworkReply.NoError:
                self.v2rayAPI.setv2raycoreAPI(str(reply.readAll(), "utf-8"))
                self.createDownloadINFO()
            else:
                self.v2rayAPI.setV2raycoreErrorString(reply.errorString())
                self.v2rayAPI.setv2raycoreAPI(False)
            self.v2rayAPI.checkDownloadInfo.emit()

    def createDownloadINFO(self):
        api = None
        try:
            api = json.loads(self.v2rayAPI.getv2raycoreAPI())
        except Exception:
            api = None
        if (api):
            try:
                # ## this code for get Latest release Download Files' path
                for i in api["assets"]:
                    self.v2rayAPI.setdownloadINFO(i["name"],
                                                  i["browser_download_url"])
                self.v2rayAPI.setV2raycoreVersion(api["tag_name"])
            except Exception:
                pass

    def setautoupdateProxy(self, proxy):
        self.proxy = QNetworkProxy()
        protocol = copy.deepcopy(proxy[0])
        proxyhostName = copy.deepcopy(proxy[1])
        proxyPort = copy.deepcopy(proxy[2])

        self.proxy.setType(protocol)
        self.proxy.setHostName(proxyhostName)
        self.proxy.setPort(int(proxyPort))
        self.proxy.setApplicationProxy(self.proxy)

    def getcurrentTime(self):
        currentTime = False
        if updateV2rayQTime().isMorning():
            currentTime = 1
        elif updateV2rayQTime().isAfternoon():
            currentTime = 2
        elif updateV2rayQTime().isEvening():
            currentTime = 3
        elif updateV2rayQTime().isNight():
            currentTime = 4
        return currentTime

    def checkDonwloadinfo(self):
        self.getV2raycoreInfoFromGithub(self.v2raycoreAPIURL)
        self.v2rayAPI.checkDownloadInfo.connect(
            lambda: self.getdownloadPath(self.usingVersion))

    def getdownloadPath(self, usingVersion):
        download = False
        if (self.v2rayAPI.getv2raycoreAPI()):
            self.downloadPath = False
            for file, filePath in self.v2rayAPI.getdownloadINFO().items():
                if self.downloadFile == file:
                    self.downloadPath = copy.deepcopy(filePath)
                    break
            self.latestVersion = copy.deepcopy(
                self.v2rayAPI.getV2raycoreVersion())

            if not usingVersion:
                download = True
            elif self.checkNewestfileDownload(usingVersion,
                                              self.latestVersion):
                download = True

            if (download and self.downloadPath):
                self.downloadV2rayCoreNow(self.downloadFile, self.downloadPath,
                                          self.latestVersion)

    def downloadV2rayCoreNow(self,
                             downloadFile=False,
                             downloadPath=False,
                             latestVersion=False,
                             bridgetreasureChest=False,
                             bridgeSingal=False):
        if not downloadPath and not latestVersion and not downloadFile:
            return False
        if (bridgetreasureChest):
            self.bridgetreasureChest = bridgetreasureChest

        if (bridgeSingal):
            self.startV2ray = bridgeSingal[0]
            self.stopV2ray = bridgeSingal[1]

        self.checkdownloadFileExists(downloadPath)
        self.downloadV2raycore(QUrl(downloadPath))
        self.downloadFinish.connect(
            lambda: self.installDownloadFile(downloadFile, latestVersion))

    def installDownloadFile(self, downloadFile, latestVersion):
        if self.installOption == "manual":
            self.msgBox.information(
                QDialog().move(self.fly),
                self.translate("updateV2ray", "update"),
                self.translate(
                    "updateV2ray",
                    "The newest v2ray-core: {} .\nversion: {} was downloaded,\nPlease check."
                ).format(downloadFile, latestVersion))
        elif self.installOption == "auto":
            if self.unzipdownloadFile(
                    downloadFile, latestVersion) and (not self.silentInstall):
                self.msgBox.information(
                    QDialog().move(self.fly),
                    self.translate("updateV2ray", "update"),
                    self.translate(
                        "updateV2ray",
                        "The newest v2ray-core: {} .\n version: {} was installed. \nPlease restart V2ray-shell"
                    ).format(downloadFile, latestVersion))

    def checkdownloadFileExists(self, downloadPath):
        if (not downloadPath or not downloadPath): return False
        filePath = QUrl(downloadPath)
        fileInfo = QFileInfo(filePath.path())
        fileName = fileInfo.fileName()
        if QFile.exists(fileName):
            QFile.remove(fileName)
            return True

    def unzipdownloadFile(self, downladFile, latestVersion):
        import zipfile
        fileInfo = None
        self.newV2rayPath = None
        if QFile.exists(downladFile):
            fileInfo = QFileInfo(QFile(downladFile))
        else:
            return False

        def checkFilesize(file):
            v2rayFile = QFile(file.absoluteFilePath())
            # check file size need open the file
            v2rayFile.open(QIODevice.ReadOnly | QIODevice.Text)
            if v2rayFile.error() == v2rayFile.NoError:
                if v2rayFile.size() > 600000:
                    v2rayFile.close()
                    return True
            else:
                v2rayFile.close()
                return False

        if (fileInfo):
            with zipfile.ZipFile(fileInfo.absoluteFilePath(), "r") as zip_ref:
                for i in zip_ref.namelist():
                    absoluteFilePath = fileInfo.absolutePath(
                    ) + QDir.separator() + i
                    if re.search("/v2ray.exe$",
                                 absoluteFilePath):  # ## windows
                        self.newV2rayPath = None
                        self.newV2rayPath = QFileInfo(QFile(absoluteFilePath))
                        if self.newV2rayPath and checkFilesize(
                                self.newV2rayPath):
                            break
                    if re.search("/v2ray$", absoluteFilePath):  # ## other
                        self.newV2rayPath = None
                        self.newV2rayPath = QFileInfo(QFile(absoluteFilePath))
                        if self.newV2rayPath and checkFilesize(
                                self.newV2rayPath):
                            break
                zip_ref.extractall(fileInfo.absolutePath())
                if sys.platform.startswith('win'): pass
                else:
                    os.chmod(self.newV2rayPath.absoluteFilePath(), 0o755)
                    os.chmod(
                        self.newV2rayPath.absoluteFilePath()[:-5] + "v2ctl",
                        0o755)
            if self.newV2rayPath:
                if (self.stopV2ray): self.stopV2ray.emit()
                self.bridgetreasureChest.setV2raycoreFilePath(
                    self.newV2rayPath.absoluteFilePath())
                self.bridgetreasureChest.setV2raycoreVersion(latestVersion)
                self.bridgetreasureChest.save.emit()
                if (self.startV2ray): self.startV2ray.emit()
                return True
            else:
                return False

    def checkNewestfileDownload(self, usingVersion, latestVersion):
        if not usingVersion: return True
        v = re.search("v", str(usingVersion))
        if (v):
            usingVersion = usingVersion[1:].split('.')
        else:
            return False
        del v
        v = re.search("v", str(latestVersion))
        if (v):
            latestVersion = latestVersion[1:].split('.')
        else:
            return False
        latestMilestone, latestRelease = int(latestVersion[0]), int(
            latestVersion[1])

        usingMilestone, usingRelease = int(usingVersion[0]), int(
            usingVersion[1])

        if (latestMilestone > usingMilestone):
            return True

        if ((latestMilestone == usingMilestone)
                and (latestRelease > usingRelease)):
            return True

        return False

    def partsoftheDay(self):
        """
        Morning   :  05:00 to 12:00 (5, 6, 7, 8, 9, 10, 11, 12)
        Afternoon :  13:00 to 17:00 (13, 14, 15, 16, 17)
        Evening   :  18:00 to 21:00 (18, 19, 20, 21)
        Night     :  22:00 to 04:00 (22, 23, 0, 1, 2, 3, 4)
        """
        return (self.translate("updateV2ray", "Morning"),
                self.translate("updateV2ray", "Afternoon"),
                self.translate("updateV2ray", "Evening"),
                self.translate("updateV2ray", "Night"))
Exemple #43
0
 def __init__(self, url):
     QNetworkAccessManager.__init__(self)
     self.request = QNetworkRequest(QUrl(url))
     self.reply = self.get(self.request)
Exemple #44
0
class OpencvWidget(QLabel):
    def __init__(self, *args, **kwargs):
        super(OpencvWidget, self).__init__(*args, **kwargs)
        self.httpRequestAborted = False
        self.fps = 24
        self.resize(800, 600)

        if not os.path.exists(
                "D:/access55/shape_predictor_68_face_landmarks.dat"):
            self.setText("正在下载数据文件。。。")
            self.outFile = QFile(
                "D:/access55/shape_predictor_68_face_landmarks.dat.bz2")
            if not self.outFile.open(QIODevice.WriteOnly):
                QMessageBox.critical(self, '错误', '无法写入文件')
                return
            self.qnam = QNetworkAccessManager(self)
            self._reply = self.qnam.get(QNetworkRequest(QUrl(URL)))
            self._reply.finished.connect(self.httpFinished)
            self._reply.readyRead.connect(self.httpReadyRead)
            self._reply.downloadProgress.connect(self.updateDataReadProgress)
        else:
            self.startCapture()

    def httpFinished(self):
        self.outFile.close()
        if self.httpRequestAborted or self._reply.error():
            self.outFile.remove()
        self._reply.deleteLater()
        del self._reply
        # 下载完成解压文件并加载摄像头
        self.setText("正在解压数据。。。")
        try:
            bz = BZ2Decompressor()
            data = bz.decompress(
                open('D:/access55/shape_predictor_68_face_landmarks.dat.bz2',
                     'rb').read())
            open('D:/access55/shape_predictor_68_face_landmarks.dat',
                 'wb').write(data)
        except Exception as e:
            self.setText('解压失败:' + str(e))
            return
        self.setText('正在开启摄像头。。。')
        self.startCapture()

    def httpReadyRead(self):
        self.outFile.write(self._reply.readAll())
        self.outFile.flush()

    def updateDataReadProgress(self, bytesRead, totalBytes):
        self.setText('已下载:{} %'.format(round(bytesRead / 64040097 * 100, 2)))

    def startCapture(self):
        self.setText("请稍候,正在初始化数据和摄像头。。。")
        try:
            # 检测相关
            self.detector = dlib.get_frontal_face_detector()
            self.predictor = dlib.shape_predictor(
                "D:/access55/shape_predictor_68_face_landmarks.dat")
            cascade_fn = "D:/access55/lbpcascades/lbpcascade_frontalface.xml"
            self.cascade = cv2.CascadeClassifier(cascade_fn)
            if not self.cascade:
                return QMessageBox.critical(self, "错误", cascade_fn + " 无法找到")
            self.cap = cv2.VideoCapture(0)
            if not self.cap or not self.cap.isOpened():
                return QMessageBox.critical(self, "错误", "打开摄像头失败")
            # 开启定时器定时捕获
            self.timer = QTimer(self, timeout=self.onCapture)
            self.timer.start(1000 / self.fps)
        except Exception as e:
            QMessageBox.critical(self, "错误", str(e))

    def closeEvent(self, event):
        if hasattr(self, "_reply") and self._reply:
            self.httpRequestAborted = True
            self._reply.abort()
            try:
                os.unlink(
                    "D:/access55/shape_predictor_68_face_landmarks.dat.bz2")
            except:
                pass
            try:
                os.unlink("D:/access55/shape_predictor_68_face_landmarks.dat")
            except:
                pass
        if hasattr(self, "timer"):
            self.timer.stop()
            self.timer.deleteLater()
            self.cap.release()
            del self.predictor, self.detector, self.cascade, self.cap
        super(OpencvWidget, self).closeEvent(event)
        self.deleteLater()

    def onCapture(self):
        _, frame = self.cap.read()

        minisize = (int(frame.shape[1] / DOWNSCALE),
                    int(frame.shape[0] / DOWNSCALE))
        tmpframe = cv2.resize(frame, minisize)
        tmpframe = cv2.cvtColor(tmpframe, cv2.COLOR_BGR2GRAY)  # 做灰度处理
        tmpframe = cv2.equalizeHist(tmpframe)

        # minNeighbors表示每一个目标至少要被检测到5次
        faces = self.cascade.detectMultiScale(tmpframe, minNeighbors=5)
        del tmpframe
        if len(faces) < 1:  # 没有检测到脸
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = QImage(frame.data, frame.shape[1], frame.shape[0],
                         frame.shape[1] * 3, QImage.Format_RGB888)
            del frame
            return self.setPixmap(QPixmap.fromImage(img))
        # 特征点检测描绘
        for x, y, w, h in faces:
            x, y, w, h = x * DOWNSCALE, y * DOWNSCALE, w * DOWNSCALE, h * DOWNSCALE
            # 画脸矩形
            cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0))
            # 截取的人脸部分
            tmpframe = frame[y:y + h, x:x + w]
            # 进行特征点描绘
            rects = self.detector(tmpframe, 1)
            if len(rects) > 0:
                landmarks = numpy.matrix(
                    [[p.x, p.y]
                     for p in self.predictor(tmpframe, rects[0]).parts()])
                for _, point in enumerate(landmarks):
                    pos = (point[0, 0] + x, point[0, 1] + y)
                    # 在原来画面上画点
                    cv2.circle(frame, pos, 3, color=(0, 255, 0))
            # 转成Qt能显示的
            frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
            img = QImage(frame.data, frame.shape[1], frame.shape[0],
                         frame.shape[1] * 3, QImage.Format_RGB888)
            del frame
            self.setPixmap(QPixmap.fromImage(img))
Exemple #45
0
class PDFWidget(QLabel):

    """
    A widget showing one page of a PDF.
    If you want to show multiple pages of the same PDF,
    make sure you share the document (let the first PDFWidget
    create the document, then pass thatPDFwidget.document to any
    subsequent widgets you create) or use a ScrolledPDFWidget.
    Will try to resize to a reasonable size to fit inside the
    geometry passed in (typically the screen size, a PyQt5.QtCore.QRect);
    or specify dpi explicitly.
    """

    def __init__(self, url, document=None, pageno=1, dpi=None,
                 geometry=None, parent=None, load_cb=None):
        """
           load_cb: will be called when the document is loaded.
        """
        super().__init__(parent)

        self.geometry = geometry

        # Guess at initial size: will be overridden later.
        if geometry:
            self.winwidth = geometry.height() * .75
            self.winheight = geometry.height()
        else:
            self.geometry = QSize(600, 800)
            self.winwidth = 600
            self.winheight = 800

        self.filename = url

        self.load_cb = load_cb

        self.network_manager = None

        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        if not document:
            self.document = None
            if url:
                self.start_load(url)
        else:
            self.document = document
        self.page = None
        self.pagesize = QSize(self.winwidth, self.winheight)

        self.dpi = dpi

        # Poppler page numbering starts from 0 but that's not what
        # most PDF users will expect, so subtract:
        if pageno > 0:
            pageno -= 1
        self.pageno = pageno

        self.render()

    def sizeHint(self):
        if not self.page:
            if not self.document:
                return QSize(self.winwidth, self.winheight)
            self.page = self.document.page(self.pageno)

        if not self.pagesize:
            self.pagesize = self.page.pageSize()

        return self.pagesize

    def render(self):
        """Render to a pixmap at the current DPI setting.
        """
        if not self.document:
            return

        if not self.page:
            self.page = self.document.page(self.pageno)
            self.pagesize = self.page.pageSize()

            self.document.setRenderHint(Poppler.Document.TextAntialiasing)
            # self.document.setRenderHint(Poppler.Document.TextHinting)

        if not self.dpi:
            # Probably first time here.
            # self.pagesize is sized in pixels assuming POINTS_PER_INCH;
            # adjust that so the page barely fits on in self.geometry.

            # First assume that it's portrait aspect ratio and that
            # vertical size will be the limiting factor.
            self.dpi = POINTS_PER_INCH * \
                self.geometry.height() / self.pagesize.height()

            # Was that too much: will it overflow in width?
            if self.pagesize.width() * self.dpi / POINTS_PER_INCH \
               > self.geometry.width():
                self.dpi = POINTS_PER_INCH * \
                    self.geometry.width() / self.pagesize.width()

            self.winwidth = self.pagesize.width() * self.dpi / POINTS_PER_INCH
            self.winheight = self.pagesize.height() * self.dpi / POINTS_PER_INCH

            # Most Qt5 programs seem to use setGeometry(x, y, w, h)
            # to set initial window size. resize() is the only method I've
            # found that doesn't force initial position as well as size.
            self.resize(self.winwidth, self.winheight)

        self.setWindowTitle('PDF Viewer')

        img = self.page.renderToImage(self.dpi, self.dpi)
        self.pixmap = QPixmap.fromImage(img)
        self.setPixmap(self.pixmap)

    def start_load(self, url):
        """Create a Poppler.Document from the given URL, QUrl or filename.
           Return, then asynchronously call self.load_cb.
        """

        # If it's not a local file, we'll need to load it.
        # http://doc.qt.io/qt-5/qnetworkaccessmanager.html
        qurl = QUrl(url)
        if not qurl.scheme():
            qurl = QUrl.fromLocalFile(url)
        if not self.network_manager:
            self.network_manager = QNetworkAccessManager();
        self.network_manager.finished.connect(self.download_finished)
        self.network_manager.get(QNetworkRequest(qurl))

    def download_finished(self, network_reply):
        qbytes = network_reply.readAll()
        self.document = Poppler.Document.loadFromData(qbytes)

        self.render()

        if self.load_cb:
            self.load_cb()
Exemple #46
0
class LauncherUpdateDialog(QDialog):
    def __init__(self, url, version, parent=0, f=0):
        super(LauncherUpdateDialog, self).__init__(parent, f)

        self.updated = False
        self.url = url

        layout = QGridLayout()

        self.shown = False
        self.qnam = QNetworkAccessManager()
        self.http_reply = None

        progress_label = QLabel()
        progress_label.setText(_('Progress:'))
        layout.addWidget(progress_label, 0, 0, Qt.AlignRight)
        self.progress_label = progress_label

        progress_bar = QProgressBar()
        layout.addWidget(progress_bar, 0, 1)
        self.progress_bar = progress_bar

        url_label = QLabel()
        url_label.setText(_('Url:'))
        layout.addWidget(url_label, 1, 0, Qt.AlignRight)
        self.url_label = url_label

        url_lineedit = QLineEdit()
        url_lineedit.setText(url)
        url_lineedit.setReadOnly(True)
        layout.addWidget(url_lineedit, 1, 1)
        self.url_lineedit = url_lineedit

        size_label = QLabel()
        size_label.setText(_('Size:'))
        layout.addWidget(size_label, 2, 0, Qt.AlignRight)
        self.size_label = size_label

        size_value_label = QLabel()
        layout.addWidget(size_value_label, 2, 1)
        self.size_value_label = size_value_label

        speed_label = QLabel()
        speed_label.setText(_('Speed:'))
        layout.addWidget(speed_label, 3, 0, Qt.AlignRight)
        self.speed_label = speed_label

        speed_value_label = QLabel()
        layout.addWidget(speed_value_label, 3, 1)
        self.speed_value_label = speed_value_label

        cancel_button = QPushButton()
        cancel_button.setText(_('Cancel update'))
        cancel_button.setStyleSheet('font-size: 15px;')
        cancel_button.clicked.connect(self.cancel_update)
        layout.addWidget(cancel_button, 4, 0, 1, 2)
        self.cancel_button = cancel_button

        layout.setColumnStretch(1, 100)

        self.setLayout(layout)
        self.setMinimumSize(300, 0)
        self.setWindowTitle(_('CDDA Game Launcher self-update'))

    def showEvent(self, event):
        if not self.shown:
            temp_dl_dir = tempfile.mkdtemp(prefix=cons.TEMP_PREFIX)

            exe_name = os.path.basename(sys.executable)

            self.downloaded_file = os.path.join(temp_dl_dir, exe_name)
            self.downloading_file = open(self.downloaded_file, 'wb')

            self.download_last_read = datetime.utcnow()
            self.download_last_bytes_read = 0
            self.download_speed_count = 0
            self.download_aborted = False

            self.http_reply = self.qnam.get(QNetworkRequest(QUrl(self.url)))
            self.http_reply.finished.connect(self.http_finished)
            self.http_reply.readyRead.connect(self.http_ready_read)
            self.http_reply.downloadProgress.connect(self.dl_progress)

        self.shown = True

    def closeEvent(self, event):
        self.cancel_update(True)

    def http_finished(self):
        self.downloading_file.close()

        if self.download_aborted:
            download_dir = os.path.dirname(self.downloaded_file)
            delete_path(download_dir)
        else:
            redirect = self.http_reply.attribute(
                QNetworkRequest.RedirectionTargetAttribute)
            if redirect is not None:
                download_dir = os.path.dirname(self.downloaded_file)
                delete_path(download_dir)
                os.makedirs(download_dir)

                redirected_url = urljoin(
                    self.http_reply.request().url().toString(),
                    redirect.toString())

                self.downloading_file = open(self.downloaded_file, 'wb')

                self.download_last_read = datetime.utcnow()
                self.download_last_bytes_read = 0
                self.download_speed_count = 0
                self.download_aborted = False

                self.progress_bar.setValue(0)

                self.http_reply = self.qnam.get(
                    QNetworkRequest(QUrl(redirected_url)))
                self.http_reply.finished.connect(self.http_finished)
                self.http_reply.readyRead.connect(self.http_ready_read)
                self.http_reply.downloadProgress.connect(self.dl_progress)
            else:
                # Download completed
                if getattr(sys, 'frozen', False):
                    # Launch self.downloaded_file and close

                    subprocess.Popen([self.downloaded_file])

                    self.updated = True
                    self.done(0)

    def http_ready_read(self):
        self.downloading_file.write(self.http_reply.readAll())

    def dl_progress(self, bytes_read, total_bytes):
        self.progress_bar.setMaximum(total_bytes)
        self.progress_bar.setValue(bytes_read)

        self.download_speed_count += 1

        self.size_value_label.setText('{bytes_read}/{total_bytes}'.format(
            bytes_read=sizeof_fmt(bytes_read),
            total_bytes=sizeof_fmt(total_bytes)))

        if self.download_speed_count % 5 == 0:
            delta_bytes = bytes_read - self.download_last_bytes_read
            delta_time = datetime.utcnow() - self.download_last_read

            bytes_secs = delta_bytes / delta_time.total_seconds()
            self.speed_value_label.setText(
                _('{bytes_sec}/s').format(bytes_sec=sizeof_fmt(bytes_secs)))

            self.download_last_bytes_read = bytes_read
            self.download_last_read = datetime.utcnow()

    def cancel_update(self, from_close=False):
        if self.http_reply.isRunning():
            self.download_aborted = True
            self.http_reply.abort()

        if not from_close:
            self.close()
Exemple #47
0
def qnam(qapp):
    """Session-wide QNetworkAccessManager."""
    from PyQt5.QtNetwork import QNetworkAccessManager
    nam = QNetworkAccessManager()
    nam.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
    return nam
Exemple #48
0
class TabbedWindow(QMainWindow):
    def __init__(self, title):
        super(TabbedWindow, self).__init__()

        self.setMinimumSize(440, 540)

        self.create_status_bar()
        self.create_central_widget()
        self.create_menu()

        self.shown = False
        self.qnam = QNetworkAccessManager()
        self.http_reply = None
        self.in_manual_update_check = False

        self.faq_dialog = None
        self.about_dialog = None

        geometry = get_config_value('window_geometry')
        if geometry is not None:
            qt_geometry = QByteArray.fromBase64(geometry.encode('utf8'))
            self.restoreGeometry(qt_geometry)

        self.setWindowTitle(title)

        if not config_true(
                get_config_value('allow_multiple_instances', 'False')):
            self.init_named_pipe()

    def set_text(self):
        self.file_menu.setTitle(_('&File'))
        self.exit_action.setText(_('E&xit'))
        self.help_menu.setTitle(_('&Help'))
        self.faq_action.setText(_('&Frequently asked questions (FAQ)'))
        self.game_issue_action.setText(_('&Game issue'))
        if getattr(sys, 'frozen', False):
            self.update_action.setText(_('&Check for update'))
        self.about_action.setText(_('&About CDDA Game Launcher'))

        if self.about_dialog is not None:
            self.about_dialog.set_text()
        self.central_widget.set_text()

    def create_status_bar(self):
        status_bar = self.statusBar()
        status_bar.busy = 0

        status_bar.showMessage(_('Ready'))

    def create_central_widget(self):
        central_widget = CentralWidget()
        self.setCentralWidget(central_widget)
        self.central_widget = central_widget

    def create_menu(self):
        file_menu = QMenu(_('&File'))
        self.menuBar().addMenu(file_menu)
        self.file_menu = file_menu

        exit_action = QAction(_('E&xit'), self, triggered=self.close)
        file_menu.addAction(exit_action)
        self.exit_action = exit_action

        help_menu = QMenu(_('&Help'))
        self.menuBar().addMenu(help_menu)
        self.help_menu = help_menu

        faq_action = QAction(_('&Frequently asked questions (FAQ)'),
                             self,
                             triggered=self.show_faq_dialog)
        self.faq_action = faq_action
        self.help_menu.addAction(faq_action)

        self.help_menu.addSeparator()

        game_issue_action = QAction(_('&Game issue'),
                                    self,
                                    triggered=self.open_game_issue_url)
        self.game_issue_action = game_issue_action
        self.help_menu.addAction(game_issue_action)

        self.help_menu.addSeparator()

        if getattr(sys, 'frozen', False):
            update_action = QAction(_('&Check for update'),
                                    self,
                                    triggered=self.manual_update_check)
            self.update_action = update_action
            self.help_menu.addAction(update_action)

        about_action = QAction(_('&About CDDA Game Launcher'),
                               self,
                               triggered=self.show_about_dialog)
        self.about_action = about_action
        self.help_menu.addAction(about_action)

    def open_game_issue_url(self):
        QDesktopServices.openUrl(QUrl(cons.GAME_ISSUE_URL))

    def show_faq_dialog(self):
        if self.faq_dialog is None:
            faq_dialog = FaqDialog(
                self, Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
            self.faq_dialog = faq_dialog

        self.faq_dialog.exec()

    def show_about_dialog(self):
        if self.about_dialog is None:
            about_dialog = AboutDialog(
                self, Qt.WindowTitleHint | Qt.WindowCloseButtonHint)
            self.about_dialog = about_dialog

        self.about_dialog.exec()

    def check_new_launcher_version(self):
        self.lv_html = BytesIO()

        url = cons.GITHUB_REST_API_URL + cons.CDDAGL_LATEST_RELEASE

        request = QNetworkRequest(QUrl(url))
        request.setRawHeader(b'User-Agent',
                             b'CDDA-Game-Launcher/' + version.encode('utf8'))
        request.setRawHeader(b'Accept', cons.GITHUB_API_VERSION)

        self.http_reply = self.qnam.get(request)
        self.http_reply.finished.connect(self.lv_http_finished)
        self.http_reply.readyRead.connect(self.lv_http_ready_read)

    def lv_http_finished(self):
        redirect = self.http_reply.attribute(
            QNetworkRequest.RedirectionTargetAttribute)
        if redirect is not None:
            redirected_url = urljoin(
                self.http_reply.request().url().toString(),
                redirect.toString())

            self.lv_html = BytesIO()

            request = QNetworkRequest(QUrl(redirected_url))
            request.setRawHeader(
                b'User-Agent', b'CDDA-Game-Launcher/' + version.encode('utf8'))
            request.setRawHeader(b'Accept', cons.GITHUB_API_VERSION)

            self.http_reply = self.qnam.get(request)
            self.http_reply.finished.connect(self.lv_http_finished)
            self.http_reply.readyRead.connect(self.lv_http_ready_read)
            return

        status_code = self.http_reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if status_code != 200:
            reason = self.http_reply.attribute(
                QNetworkRequest.HttpReasonPhraseAttribute)
            url = self.http_reply.request().url().toString()
            logger.warning(
                _('Could not find launcher latest release when requesting {url}. Error: {error}'
                  ).format(url=url, error=f'[HTTP {status_code}] ({reason})'))

            if self.in_manual_update_check:
                self.in_manual_update_check = False

            self.lv_html = None
            return

        self.lv_html.seek(0)

        try:
            latest_release = json.loads(
                TextIOWrapper(self.lv_html, encoding='utf8').read())
        except json.decoder.JSONDecodeError:
            latest_release = {'cannot_decode': True}

        self.lv_html = None

        if 'name' not in latest_release:
            return
        if 'html_url' not in latest_release:
            return
        if 'tag_name' not in latest_release:
            return
        if 'assets' not in latest_release:
            return
        if 'body' not in latest_release:
            return

        version_text = latest_release['tag_name']
        if version_text.startswith('v'):
            version_text = version_text[1:]
        latest_version = LooseVersion(version_text)

        if latest_version is None:
            return

        executable_url = None
        for file_asset in latest_release['assets']:
            if not 'name' in file_asset:
                continue
            if 'browser_download_url' not in file_asset:
                continue
            if file_asset['name'].endswith('.exe'):
                executable_url = file_asset['browser_download_url']
                break

        if executable_url is None:
            return

        current_version = LooseVersion(version)

        if latest_version > current_version:

            markdown_desc = latest_release['body']

            # Replace number signs with issue links
            number_pattern = ' \\#(?P<id>\\d+)'
            replacement_pattern = (' [#\\g<id>](' +
                                   cons.CDDAGL_ISSUE_URL_ROOT + '\\g<id>)')
            markdown_desc = re.sub(number_pattern, replacement_pattern,
                                   markdown_desc)

            html_desc = markdown.markdown(markdown_desc)

            release_html = ('''
<h2><a href="{release_url}">{release_name}</a></h2>{description}
                ''').format(release_url=html.escape(
                latest_release['html_url']),
                            release_name=html.escape(latest_release['name']),
                            description=html_desc)

            no_launcher_version_check_checkbox = QCheckBox()
            no_launcher_version_check_checkbox.setText(
                _('Do not check '
                  'for new version of the CDDA Game Launcher on launch'))
            check_state = (Qt.Checked if config_true(
                get_config_value('prevent_version_check_launch', 'False')) else
                           Qt.Unchecked)
            no_launcher_version_check_checkbox.stateChanged.connect(
                self.nlvcc_changed)
            no_launcher_version_check_checkbox.setCheckState(check_state)

            launcher_update_msgbox = QMessageBox()
            launcher_update_msgbox.setWindowTitle(_('Launcher update'))
            launcher_update_msgbox.setText(
                _('You are using version '
                  '{version} but there is a new update for CDDA Game '
                  'Launcher. Would you like to update?').format(
                      version=version))
            launcher_update_msgbox.setInformativeText(release_html)
            launcher_update_msgbox.addButton(_('Update the launcher'),
                                             QMessageBox.YesRole)
            launcher_update_msgbox.addButton(_('Not right now'),
                                             QMessageBox.NoRole)
            launcher_update_msgbox.setCheckBox(
                no_launcher_version_check_checkbox)
            launcher_update_msgbox.setIcon(QMessageBox.Question)

            if launcher_update_msgbox.exec() == 0:
                flags = Qt.WindowTitleHint | Qt.WindowCloseButtonHint

                launcher_update_dialog = (LauncherUpdateDialog(
                    executable_url, version_text, self, flags))
                launcher_update_dialog.exec()

                if launcher_update_dialog.updated:
                    self.close()

        else:
            self.no_launcher_update_found()

    def nlvcc_changed(self, state):
        no_launcher_version_check_checkbox = (
            self.central_widget.settings_tab.launcher_settings_group_box.
            no_launcher_version_check_checkbox)
        no_launcher_version_check_checkbox.setCheckState(state)

    def manual_update_check(self):
        self.in_manual_update_check = True
        self.check_new_launcher_version()

    def no_launcher_update_found(self):
        if self.in_manual_update_check:
            up_to_date_msgbox = QMessageBox()
            up_to_date_msgbox.setWindowTitle(_('Up to date'))
            up_to_date_msgbox.setText(
                _('The CDDA Game Launcher is up to date.'))
            up_to_date_msgbox.setIcon(QMessageBox.Information)

            up_to_date_msgbox.exec()

            self.in_manual_update_check = False

    def lv_http_ready_read(self):
        self.lv_html.write(self.http_reply.readAll())

    def init_named_pipe(self):
        class PipeReadWaitThread(QThread):
            read = pyqtSignal(bytes)

            def __init__(self):
                super(PipeReadWaitThread, self).__init__()

                try:
                    self.pipe = SimpleNamedPipe('cddagl_instance')
                except (OSError, PyWinError):
                    self.pipe = None

            def __del__(self):
                self.wait()

            def run(self):
                if self.pipe is None:
                    return

                while self.pipe is not None:
                    if self.pipe.connect() and self.pipe is not None:
                        try:
                            value = self.pipe.read(1024)
                            self.read.emit(value)
                        except (PyWinError, IOError):
                            pass

        def instance_read(value):
            if value == b'dupe':
                self.showNormal()
                self.raise_()
                self.activateWindow()

        pipe_read_wait_thread = PipeReadWaitThread()
        pipe_read_wait_thread.read.connect(instance_read)
        pipe_read_wait_thread.start()

        self.pipe_read_wait_thread = pipe_read_wait_thread

    def showEvent(self, event):
        if not self.shown:
            if not config_true(
                    get_config_value('prevent_version_check_launch', 'False')):
                if getattr(sys, 'frozen', False):
                    self.in_manual_update_check = False
                    self.check_new_launcher_version()

        self.shown = True

    def save_geometry(self):
        geometry = self.saveGeometry().toBase64().data().decode('utf8')
        set_config_value('window_geometry', geometry)

        backups_tab = self.central_widget.backups_tab
        backups_tab.save_geometry()

    def closeEvent(self, event):
        update_group_box = self.central_widget.main_tab.update_group_box
        soundpacks_tab = self.central_widget.soundpacks_tab

        if update_group_box.updating:
            update_group_box.close_after_update = True
            update_group_box.update_game()

            if not update_group_box.updating:
                self.save_geometry()
                event.accept()
            else:
                event.ignore()
        elif soundpacks_tab.installing_new_soundpack:
            soundpacks_tab.close_after_install = True
            soundpacks_tab.install_new()

            if not soundpacks_tab.installing_new_soundpack:
                self.save_geometry()
                event.accept()
            else:
                event.ignore()
        else:
            self.save_geometry()
            event.accept()
Exemple #49
0
    def __init__(self, parent, *args, **kwargs):
        super(ListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.env = parent.env

        self.device = None
        self.idx = None

        self.nam = QNetworkAccessManager()
        self.backup = bytes()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat)
        views_order = self.settings.value("views_order", [])

        self.views = {}
        self.settings.beginGroup("Views")
        views = self.settings.childKeys()
        if views and views_order:
            for view in views_order.split(";"):
                view_list = self.settings.value(view).split(";")
                self.views[view] = base_view + view_list
        else:
            self.views = default_views
        self.settings.endGroup()

        self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly)
        # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)

        self.pwm_sliders = []

        self.layout().addWidget(self.tb)
        self.layout().addWidget(self.tb_relays)
        # self.layout().addWidget(self.tb_filter)

        self.device_list = TableView()
        self.device_list.setIconSize(QSize(24, 24))
        self.model = parent.device_model
        self.model.setupColumns(self.views["Home"])

        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole)
        self.sorted_device_model.setSortLocaleAware(True)
        self.sorted_device_model.setFilterKeyColumn(-1)

        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupView(self.views["Home"])
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.layout().addWidget(self.device_list)

        self.layout().addWidget(self.tb_views)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu)

        self.ctx_menu = QMenu()

        self.create_actions()
        self.create_view_buttons()
        # self.create_view_filter()

        self.device_list.doubleClicked.connect(lambda: self.openConsole.emit())
Exemple #50
0
class AppsTable(QTableWidget):
    def __init__(self, parent: QWidget, icon_cache: Cache, disk_cache: bool,
                 download_icons: bool):
        super(AppsTable, self).__init__()
        self.setParent(parent)
        self.window = parent
        self.disk_cache = disk_cache
        self.download_icons = download_icons
        self.column_names = ['' for _ in range(7)]
        self.setColumnCount(len(self.column_names))
        self.setFocusPolicy(Qt.NoFocus)
        self.setShowGrid(False)
        self.verticalHeader().setVisible(False)
        self.horizontalHeader().setVisible(False)
        self.setSelectionBehavior(QTableView.SelectRows)
        self.setHorizontalHeaderLabels(self.column_names)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.icon_flathub = QIcon(resource.get_path('img/flathub.svg'))
        self.icon_logo = QIcon(resource.get_path('img/logo.svg'))

        self.network_man = QNetworkAccessManager()
        self.network_man.finished.connect(self._load_icon_and_cache)

        self.icon_cache = icon_cache
        self.lock_async_data = Lock()

    def has_any_settings(self, app_v: ApplicationView):
        return app_v.model.can_be_refreshed() or \
               app_v.model.has_history() or \
               app_v.model.can_be_downgraded()

    def show_app_settings(self, app: ApplicationView):
        menu_row = QMenu()

        if app.model.installed:
            if app.model.can_be_refreshed():
                action_history = QAction(self.window.locale_keys[
                    "manage_window.apps_table.row.actions.refresh"])
                action_history.setIcon(
                    QIcon(resource.get_path('img/refresh.svg')))

                def refresh():
                    self.window.refresh(app)

                action_history.triggered.connect(refresh)
                menu_row.addAction(action_history)

            if app.model.has_history():
                action_history = QAction(self.window.locale_keys[
                    "manage_window.apps_table.row.actions.history"])
                action_history.setIcon(
                    QIcon(resource.get_path('img/history.svg')))

                def show_history():
                    self.window.get_app_history(app)

                action_history.triggered.connect(show_history)
                menu_row.addAction(action_history)

            if app.model.can_be_downgraded():
                action_downgrade = QAction(self.window.locale_keys[
                    "manage_window.apps_table.row.actions.downgrade"])

                def downgrade():
                    if dialog.ask_confirmation(
                            title=self.window.locale_keys[
                                'manage_window.apps_table.row.actions.downgrade'],
                            body=self.window.locale_keys[
                                'manage_window.apps_table.row.actions.downgrade.popup.body']
                            .format(app.model.base_data.name),
                            locale_keys=self.window.locale_keys):
                        self.window.downgrade_app(app)

                action_downgrade.triggered.connect(downgrade)
                action_downgrade.setIcon(
                    QIcon(resource.get_path('img/downgrade.svg')))
                menu_row.addAction(action_downgrade)

        menu_row.adjustSize()
        menu_row.popup(QCursor.pos())
        menu_row.exec_()

    def fill_async_data(self):
        if self.window.apps:

            for idx, app_v in enumerate(self.window.apps):

                if app_v.visible and app_v.status == ApplicationViewStatus.LOADING and app_v.model.status == ApplicationStatus.READY:

                    if self.download_icons:
                        self.network_man.get(
                            QNetworkRequest(
                                QUrl(app_v.model.base_data.icon_url)))

                    app_name = self.item(idx, 0).text()

                    if not app_name or app_name == '...':
                        self.item(idx, 0).setText(app_v.model.base_data.name)

                    self._set_col_version(idx, app_v)
                    self._set_col_description(idx, app_v)
                    app_v.status = ApplicationViewStatus.READY

            self.window.resize_and_center()

    def get_selected_app(self) -> ApplicationView:
        return self.window.apps[self.currentRow()]

    def get_selected_app_icon(self) -> QIcon:
        return self.item(self.currentRow(), 0).icon()

    def _uninstall_app(self, app_v: ApplicationView):
        if dialog.ask_confirmation(
                title=self.window.locale_keys[
                    'manage_window.apps_table.row.actions.uninstall.popup.title'],
                body=self.window.locale_keys[
                    'manage_window.apps_table.row.actions.uninstall.popup.body']
                .format(app_v),
                locale_keys=self.window.locale_keys):
            self.window.uninstall_app(app_v)

    def _downgrade_app(self):
        selected_app = self.get_selected_app()

        if dialog.ask_confirmation(
                title=self.window.
                locale_keys['manage_window.apps_table.row.actions.downgrade'],
                body=self.window.locale_keys[
                    'manage_window.apps_table.row.actions.downgrade.popup.body']
                .format(selected_app.model.base_data.name),
                locale_keys=self.window.locale_keys):
            self.window.downgrade_app(selected_app)

    def _refresh_app(self):
        self.window.refresh(self.get_selected_app())

    def _get_app_info(self):
        self.window.get_app_info(self.get_selected_app())

    def _get_app_history(self):
        self.window.get_app_history(self.get_selected_app())

    def _install_app(self, app_v: ApplicationView):

        if dialog.ask_confirmation(
                title=self.window.locale_keys[
                    'manage_window.apps_table.row.actions.install.popup.title'],
                body=self.window.locale_keys[
                    'manage_window.apps_table.row.actions.install.popup.body'].
                format(app_v),
                locale_keys=self.window.locale_keys):

            self.window.install_app(app_v)

    def _load_icon_and_cache(self, http_response):
        icon_url = http_response.url().toString()

        icon_data = self.icon_cache.get(icon_url)
        icon_was_cached = True

        if not icon_data:
            icon_bytes = http_response.readAll()

            if not icon_bytes:
                return

            icon_was_cached = False
            pixmap = QPixmap()

            pixmap.loadFromData(icon_bytes)
            icon = QIcon(pixmap)
            icon_data = {'icon': icon, 'bytes': icon_bytes}
            self.icon_cache.add(icon_url, icon_data)

        for idx, app in enumerate(self.window.apps):
            if app.model.base_data.icon_url == icon_url:
                col_name = self.item(idx, 0)
                col_name.setIcon(icon_data['icon'])

                if self.disk_cache and app.model.supports_disk_cache():
                    if not icon_was_cached or not os.path.exists(
                            app.model.get_disk_icon_path()):
                        self.window.manager.cache_to_disk(
                            app=app.model,
                            icon_bytes=icon_data['bytes'],
                            only_icon=True)

    def update_apps(self,
                    app_views: List[ApplicationView],
                    update_check_enabled: bool = True):
        self.setRowCount(len(app_views) if app_views else 0)
        self.setEnabled(True)

        if app_views:
            for idx, app_v in enumerate(app_views):
                self._set_col_name(idx, app_v)
                self._set_col_version(idx, app_v)
                self._set_col_description(idx, app_v)
                self._set_col_type(idx, app_v)
                self._set_col_installed(idx, app_v)

                self._set_col_settings(idx, app_v)

                col_update = None

                if update_check_enabled and app_v.model.update:
                    col_update = UpdateToggleButton(app_v, self.window,
                                                    self.window.locale_keys,
                                                    app_v.model.update)

                self.setCellWidget(idx, 6, col_update)

    def _gen_row_button(self, text: str, style: str, callback) -> QWidget:
        col = QWidget()
        col_bt = QPushButton()
        col_bt.setText(text)
        col_bt.setStyleSheet('QPushButton { ' + style + '}')
        col_bt.clicked.connect(callback)

        layout = QHBoxLayout()
        layout.setContentsMargins(2, 2, 2, 0)
        layout.setAlignment(Qt.AlignCenter)
        layout.addWidget(col_bt)
        col.setLayout(layout)

        return col

    def _set_col_installed(self, idx: int, app_v: ApplicationView):

        if app_v.model.installed:
            if app_v.model.can_be_uninstalled():

                def uninstall():
                    self._uninstall_app(app_v)

                col = self._gen_row_button(
                    self.window.locale_keys['uninstall'].capitalize(),
                    INSTALL_BT_STYLE.format(back='#cc0000'), uninstall)
            else:
                col = QLabel()
                col.setPixmap((QPixmap(resource.get_path('img/checked.svg'))))
                col.setAlignment(Qt.AlignCenter)
                col.setToolTip(self.window.locale_keys['installed'])
        elif app_v.model.can_be_installed():

            def install():
                self._install_app(app_v)

            col = self._gen_row_button(
                self.window.locale_keys['install'].capitalize(),
                INSTALL_BT_STYLE.format(back='#088A08'), install)
        else:
            col = None

        self.setCellWidget(idx, 4, col)

    def _set_col_type(self, idx: int, app_v: ApplicationView):
        col_type = QLabel()
        pixmap = QPixmap(app_v.model.get_default_icon_path())
        col_type.setPixmap(
            pixmap.scaled(16, 16, Qt.KeepAspectRatio, Qt.SmoothTransformation))
        col_type.setAlignment(Qt.AlignCenter)
        col_type.setToolTip('{}: {}'.format(self.window.locale_keys['type'],
                                            app_v.model.get_type()))
        self.setCellWidget(idx, 3, col_type)

    def _set_col_version(self, idx: int, app_v: ApplicationView):
        label_version = QLabel(app_v.model.base_data.version if app_v.model.
                               base_data.version else '?')
        label_version.setAlignment(Qt.AlignCenter)

        col_version = QWidget()
        col_version.setLayout(QHBoxLayout())
        col_version.layout().addWidget(label_version)

        if app_v.model.base_data.version:
            tooltip = self.window.locale_keys[
                'version.installed'] if app_v.model.installed else self.window.locale_keys[
                    'version']
        else:
            tooltip = self.window.locale_keys['version.unknown']

        if app_v.model.update:
            label_version.setStyleSheet("color: #4EC306; font-weight: bold")
            tooltip = self.window.locale_keys['version.installed_outdated']

        if app_v.model.installed and app_v.model.base_data.version and app_v.model.base_data.latest_version and app_v.model.base_data.version < app_v.model.base_data.latest_version:
            tooltip = '{}. {}: {}'.format(
                tooltip, self.window.locale_keys['version.latest'],
                app_v.model.base_data.latest_version)
            label_version.setText(
                label_version.text() +
                ' > {}'.format(app_v.model.base_data.latest_version))

        col_version.setToolTip(tooltip)
        self.setCellWidget(idx, 1, col_version)

    def _set_col_name(self, idx: int, app_v: ApplicationView):
        col = QTableWidgetItem()
        col.setText(app_v.model.base_data.name if app_v.model.base_data.
                    name else '...')
        col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        col.setToolTip(self.window.locale_keys['app.name'].lower())

        if self.disk_cache and app_v.model.supports_disk_cache(
        ) and os.path.exists(app_v.model.get_disk_icon_path()):
            with open(app_v.model.get_disk_icon_path(), 'rb') as f:
                icon_bytes = f.read()
                pixmap = QPixmap()
                pixmap.loadFromData(icon_bytes)
                icon = QIcon(pixmap)
                self.icon_cache.add_non_existing(
                    app_v.model.base_data.icon_url, {
                        'icon': icon,
                        'bytes': icon_bytes
                    })

        elif not app_v.model.base_data.icon_url:
            icon = QIcon(app_v.model.get_default_icon_path())
        else:
            icon_data = self.icon_cache.get(app_v.model.base_data.icon_url)
            icon = icon_data['icon'] if icon_data else QIcon(
                app_v.model.get_default_icon_path())

        col.setIcon(icon)
        self.setItem(idx, 0, col)

    def _set_col_description(self, idx: int, app_v: ApplicationView):
        col = QTableWidgetItem()
        col.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        if app_v.model.base_data.description is not None or app_v.model.is_library(
        ) or app_v.model.status == ApplicationStatus.READY:
            desc = app_v.model.base_data.description
        else:
            desc = '...'

        if desc and desc != '...':
            desc = util.strip_html(desc[0:25]) + '...'

        col.setText(desc)

        if app_v.model.base_data.description:
            col.setToolTip(app_v.model.base_data.description)

        self.setItem(idx, 2, col)

    def _set_col_settings(self, idx: int, app_v: ApplicationView):
        tb = QToolBar()

        if app_v.model.has_info():

            def get_info():
                self.window.get_app_info(app_v)

            tb.addWidget(
                IconButton(icon_path=resource.get_path('img/app_info.svg'),
                           action=get_info,
                           background='#2E68D3'))

        def handle_click():
            self.show_app_settings(app_v)

        if self.has_any_settings(app_v):
            bt = IconButton(
                icon_path=resource.get_path('img/app_settings.svg'),
                action=handle_click,
                background='#12ABAB')
            tb.addWidget(bt)

        self.setCellWidget(idx, 5, tb)

    def change_headers_policy(
            self, policy: QHeaderView = QHeaderView.ResizeToContents):
        header_horizontal = self.horizontalHeader()
        for i in range(self.columnCount()):
            header_horizontal.setSectionResizeMode(i, policy)
Exemple #51
0
class ListWidget(QWidget):
    deviceSelected = pyqtSignal(TasmotaDevice)
    openRulesEditor = pyqtSignal()
    openConsole = pyqtSignal()
    openTelemetry = pyqtSignal()
    openWebUI = pyqtSignal()

    def __init__(self, parent, *args, **kwargs):
        super(ListWidget, self).__init__(*args, **kwargs)
        self.setWindowTitle("Devices list")
        self.setWindowState(Qt.WindowMaximized)
        self.setLayout(VLayout(margin=0, spacing=0))

        self.mqtt = parent.mqtt
        self.env = parent.env

        self.device = None
        self.idx = None

        self.nam = QNetworkAccessManager()
        self.backup = bytes()

        self.settings = QSettings("{}/TDM/tdm.cfg".format(QDir.homePath()), QSettings.IniFormat)
        views_order = self.settings.value("views_order", [])

        self.views = {}
        self.settings.beginGroup("Views")
        views = self.settings.childKeys()
        if views and views_order:
            for view in views_order.split(";"):
                view_list = self.settings.value(view).split(";")
                self.views[view] = base_view + view_list
        else:
            self.views = default_views
        self.settings.endGroup()

        self.tb = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_relays = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonIconOnly)
        # self.tb_filter = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)
        self.tb_views = Toolbar(Qt.Horizontal, 24, Qt.ToolButtonTextBesideIcon)

        self.pwm_sliders = []

        self.layout().addWidget(self.tb)
        self.layout().addWidget(self.tb_relays)
        # self.layout().addWidget(self.tb_filter)

        self.device_list = TableView()
        self.device_list.setIconSize(QSize(24, 24))
        self.model = parent.device_model
        self.model.setupColumns(self.views["Home"])

        self.sorted_device_model = QSortFilterProxyModel()
        self.sorted_device_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
        self.sorted_device_model.setSourceModel(parent.device_model)
        self.sorted_device_model.setSortRole(Qt.InitialSortOrderRole)
        self.sorted_device_model.setSortLocaleAware(True)
        self.sorted_device_model.setFilterKeyColumn(-1)

        self.device_list.setModel(self.sorted_device_model)
        self.device_list.setupView(self.views["Home"])
        self.device_list.setSortingEnabled(True)
        self.device_list.setWordWrap(True)
        self.device_list.setItemDelegate(DeviceDelegate())
        self.device_list.sortByColumn(self.model.columnIndex("FriendlyName"), Qt.AscendingOrder)
        self.device_list.setContextMenuPolicy(Qt.CustomContextMenu)
        self.device_list.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
        self.layout().addWidget(self.device_list)

        self.layout().addWidget(self.tb_views)

        self.device_list.clicked.connect(self.select_device)
        self.device_list.customContextMenuRequested.connect(self.show_list_ctx_menu)

        self.ctx_menu = QMenu()

        self.create_actions()
        self.create_view_buttons()
        # self.create_view_filter()

        self.device_list.doubleClicked.connect(lambda: self.openConsole.emit())

    def create_actions(self):
        actConsole = self.tb.addAction(QIcon(":/console.png"), "Console", self.openConsole.emit)
        actConsole.setShortcut("Ctrl+E")

        actRules = self.tb.addAction(QIcon(":/rules.png"), "Rules", self.openRulesEditor.emit)
        actRules.setShortcut("Ctrl+R")

        actTimers = self.tb.addAction(QIcon(":/timers.png"), "Timers", self.configureTimers)

        actButtons = self.tb.addAction(QIcon(":/buttons.png"), "Buttons", self.configureButtons)
        actButtons.setShortcut("Ctrl+B")

        actSwitches = self.tb.addAction(QIcon(":/switches.png"), "Switches", self.configureSwitches)
        actSwitches.setShortcut("Ctrl+S")

        actPower = self.tb.addAction(QIcon(":/power.png"), "Power", self.configurePower)
        actPower.setShortcut("Ctrl+P")

        # setopts = self.tb.addAction(QIcon(":/setoptions.png"), "SetOptions", self.configureSO)
        # setopts.setShortcut("Ctrl+S")

        self.tb.addSpacer()

        actTelemetry = self.tb.addAction(QIcon(":/telemetry.png"), "Telemetry", self.openTelemetry.emit)
        actTelemetry.setShortcut("Ctrl+T")

        actWebui = self.tb.addAction(QIcon(":/web.png"), "WebUI", self.openWebUI.emit)
        actWebui.setShortcut("Ctrl+U")

        self.ctx_menu.addActions([actRules, actTimers, actButtons, actSwitches, actPower, actTelemetry, actWebui])
        self.ctx_menu.addSeparator()

        self.ctx_menu_cfg = QMenu("Configure")
        self.ctx_menu_cfg.setIcon(QIcon(":/settings.png"))
        self.ctx_menu_cfg.addAction("Module", self.configureModule)
        self.ctx_menu_cfg.addAction("GPIO", self.configureGPIO)
        self.ctx_menu_cfg.addAction("Template", self.configureTemplate)
        # self.ctx_menu_cfg.addAction("Wifi", self.ctx_menu_teleperiod)
        # self.ctx_menu_cfg.addAction("Time", self.cfgTime.emit)
        # self.ctx_menu_cfg.addAction("MQTT", self.ctx_menu_teleperiod)

        # self.ctx_menu_cfg.addAction("Logging", self.ctx_menu_teleperiod)

        self.ctx_menu.addMenu(self.ctx_menu_cfg)
        self.ctx_menu.addSeparator()

        self.ctx_menu.addAction(QIcon(":/refresh.png"), "Refresh", self.ctx_menu_refresh)

        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon(":/clear.png"), "Clear retained", self.ctx_menu_clear_retained)
        self.ctx_menu.addAction("Clear Backlog", self.ctx_menu_clear_backlog)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon(":/copy.png"), "Copy", self.ctx_menu_copy)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon(":/restart.png"), "Restart", self.ctx_menu_restart)
        self.ctx_menu.addAction(QIcon(), "Reset", self.ctx_menu_reset)
        self.ctx_menu.addSeparator()
        self.ctx_menu.addAction(QIcon(":/delete.png"), "Delete", self.ctx_menu_delete_device)

        # self.tb.addAction(QIcon(), "Multi Command", self.ctx_menu_webui)

        self.agAllPower = QActionGroup(self)
        self.agAllPower.addAction(QIcon(":/P_ON.png"), "All ON")
        self.agAllPower.addAction(QIcon(":/P_OFF.png"), "All OFF")
        self.agAllPower.setEnabled(False)
        self.agAllPower.setExclusive(False)
        self.agAllPower.triggered.connect(self.toggle_power_all)
        self.tb_relays.addActions(self.agAllPower.actions())

        self.agRelays = QActionGroup(self)
        self.agRelays.setVisible(False)
        self.agRelays.setExclusive(False)

        for a in range(1, 9):
            act = QAction(QIcon(":/P{}_OFF.png".format(a)), "")
            act.setShortcut("F{}".format(a))
            self.agRelays.addAction(act)

        self.agRelays.triggered.connect(self.toggle_power)
        self.tb_relays.addActions(self.agRelays.actions())

        self.tb_relays.addSeparator()
        self.actColor = self.tb_relays.addAction(QIcon(":/color.png"), "Color", self.set_color)
        self.actColor.setEnabled(False)

        self.actChannels = self.tb_relays.addAction(QIcon(":/sliders.png"), "Channels")
        self.actChannels.setEnabled(False)
        self.mChannels = QMenu()
        self.actChannels.setMenu(self.mChannels)
        self.tb_relays.widgetForAction(self.actChannels).setPopupMode(QToolButton.InstantPopup)

    def create_view_buttons(self):
        self.tb_views.addWidget(QLabel("View mode: "))
        ag_views = QActionGroup(self)
        ag_views.setExclusive(True)
        for v in self.views.keys():
            a = QAction(v)
            a.triggered.connect(self.change_view)
            a.setCheckable(True)
            ag_views.addAction(a)
        self.tb_views.addActions(ag_views.actions())
        ag_views.actions()[0].setChecked(True)

        stretch = QWidget()
        stretch.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum))
        self.tb_views.addWidget(stretch)
        # actEditView = self.tb_views.addAction("Edit views...")

    # def create_view_filter(self):
    #     # self.tb_filter.addWidget(QLabel("Show devices: "))
    #     # self.cbxLWT = QComboBox()
    #     # self.cbxLWT.addItems(["All", "Online"d, "Offline"])
    #     # self.cbxLWT.currentTextChanged.connect(self.build_filter_regex)
    #     # self.tb_filter.addWidget(self.cbxLWT)
    #
    #     self.tb_filter.addWidget(QLabel(" Search: "))
    #     self.leSearch = QLineEdit()
    #     self.leSearch.setClearButtonEnabled(True)
    #     self.leSearch.textChanged.connect(self.build_filter_regex)
    #     self.tb_filter.addWidget(self.leSearch)
    #
    # def build_filter_regex(self, txt):
    #     query = self.leSearch.text()
    #     # if self.cbxLWT.currentText() != "All":
    #     #     query = "{}|{}".format(self.cbxLWT.currentText(), query)
    #     self.sorted_device_model.setFilterRegExp(query)

    def change_view(self, a=None):
        view = self.views[self.sender().text()]
        self.model.setupColumns(view)
        self.device_list.setupView(view)

    def ctx_menu_copy(self):
        if self.idx:
            string = dumps(self.model.data(self.idx))
            if string.startswith('"') and string.endswith('"'):
                string = string[1:-1]
            QApplication.clipboard().setText(string)

    def ctx_menu_clear_retained(self):
        if self.device:
            relays = self.device.power()
            if relays and len(relays.keys()) > 0:
                for r in relays.keys():
                    self.mqtt.publish(self.device.cmnd_topic(r), retain=True)
            QMessageBox.information(self, "Clear retained", "Cleared retained messages.")

    def ctx_menu_clear_backlog(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("backlog"), "")
            QMessageBox.information(self, "Clear Backlog", "Backlog cleared.")

    def ctx_menu_restart(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("restart"), payload="1")
            for k in list(self.device.power().keys()):
                self.device.p.pop(k)

    def ctx_menu_reset(self):
        if self.device:
            reset, ok = QInputDialog.getItem(self, "Reset device and restart", "Select reset mode", resets, editable=False)
            if ok:
                self.mqtt.publish(self.device.cmnd_topic("reset"), payload=reset.split(":")[0])
                for k in list(self.device.power().keys()):
                    self.device.p.pop(k)

    def ctx_menu_refresh(self):
        if self.device:
            for k in list(self.device.power().keys()):
                self.device.p.pop(k)

            for c in initial_commands():
                cmd, payload = c
                cmd = self.device.cmnd_topic(cmd)
                self.mqtt.publish(cmd, payload, 1)

    def ctx_menu_delete_device(self):
        if self.device:
            if QMessageBox.question(self, "Confirm", "Do you want to remove the following device?\n'{}' ({})"
                    .format(self.device.p['FriendlyName1'], self.device.p['Topic'])) == QMessageBox.Yes:
                self.model.deleteDevice(self.idx)

    def ctx_menu_teleperiod(self):
        if self.device:
            teleperiod, ok = QInputDialog.getInt(self, "Set telemetry period", "Input 1 to reset to default\n[Min: 10, Max: 3600]", self.device.p['TelePeriod'], 1, 3600)
            if ok:
                if teleperiod != 1 and teleperiod < 10:
                    teleperiod = 10
            self.mqtt.publish(self.device.cmnd_topic("teleperiod"), teleperiod)

    def ctx_menu_config_backup(self):
        if self.device:
            self.device_username = self.settings.value("device_username", "", str)
            self.device_password = self.settings.value("device_password", "", str)
            self.backup = bytes()
            self.dl = self.nam.get(QNetworkRequest(QUrl("http://{}:{}@{}/dl".format( \
                self.device_username, urllib.parse.quote(self.device_password),self.device.p['IPAddress']))))
            self.dl.readyRead.connect(self.get_dump)
            self.dl.finished.connect(self.save_dump)

    def ctx_menu_ota_set_url(self):
        if self.device:
            url, ok = QInputDialog.getText(self, "Set OTA URL", '100 chars max. Set to "1" to reset to default.', text=self.device.p['OtaUrl'])
            if ok:
                self.mqtt.publish(self.device.cmnd_topic("otaurl"), payload=url)

    def ctx_menu_ota_set_upgrade(self):
        if self.device:
            if QMessageBox.question(self, "OTA Upgrade", "Are you sure to OTA upgrade from\n{}".format(self.device.p['OtaUrl']), QMessageBox.Yes | QMessageBox.No) == QMessageBox.Yes:
                self.mqtt.publish(self.device.cmnd_topic("upgrade"), payload="1")

    def show_list_ctx_menu(self, at):
        self.select_device(self.device_list.indexAt(at))
        self.ctx_menu.popup(self.device_list.viewport().mapToGlobal(at))

    def select_device(self, idx):
        self.idx = self.sorted_device_model.mapToSource(idx)
        self.device = self.model.deviceAtRow(self.idx.row())
        self.deviceSelected.emit(self.device)

        relays = self.device.power()

        self.agAllPower.setEnabled(len(relays) >= 1)

        for i, a in enumerate(self.agRelays.actions()):
            a.setVisible(len(relays) > 1 and i < len(relays))

        color = self.device.color().get("Color", False)
        has_color = bool(color)
        self.actColor.setEnabled(has_color and not self.device.setoption(68))

        self.actChannels.setEnabled(has_color)

        if has_color:
            self.actChannels.menu().clear()

            max_val = 100
            if self.device.setoption(15) == 0:
                max_val = 1023

            for k, v in self.device.pwm().items():
                channel = SliderAction(self, k)
                channel.slider.setMaximum(max_val)
                channel.slider.setValue(int(v))
                self.mChannels.addAction(channel)
                channel.slider.valueChanged.connect(self.set_channel)

            dimmer = self.device.color().get("Dimmer")
            if dimmer:
                saDimmer = SliderAction(self, "Dimmer")
                saDimmer.slider.setValue(int(dimmer))
                self.mChannels.addAction(saDimmer)
                saDimmer.slider.valueChanged.connect(self.set_channel)

    def toggle_power(self, action):
        if self.device:
            idx = self.agRelays.actions().index(action)
            relay = sorted(list(self.device.power().keys()))[idx]
            self.mqtt.publish(self.device.cmnd_topic(relay), "toggle")

    def toggle_power_all(self, action):
        if self.device:
            idx = self.agAllPower.actions().index(action)
            for r in sorted(self.device.power().keys()):
                self.mqtt.publish(self.device.cmnd_topic(r), idx ^ 1)

    def set_color(self):
        if self.device:
            color = self.device.color().get("Color")
            if color:
                dlg = QColorDialog()
                new_color = dlg.getColor(QColor("#{}".format(color)))
                if new_color.isValid():
                    new_color = new_color.name()
                    if new_color != color:
                        self.mqtt.publish(self.device.cmnd_topic("color"), new_color)

    def set_channel(self, value=0):
        cmd = self.sender().objectName()

        if self.device:
            self.mqtt.publish(self.device.cmnd_topic(cmd), str(value))

    def configureSO(self):
        if self.device:
            dlg = SetOptionsDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureModule(self):
        if self.device:
            dlg = ModuleDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureGPIO(self):
        if self.device:
            dlg = GPIODialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureTemplate(self):
        if self.device:
            dlg = TemplateDialog(self.device)
            dlg.sendCommand.connect(self.mqtt.publish)
            dlg.exec_()

    def configureTimers(self):
        if self.device:
            self.mqtt.publish(self.device.cmnd_topic("timers"))
            timers = TimersDialog(self.device)
            self.mqtt.messageSignal.connect(timers.parseMessage)
            timers.sendCommand.connect(self.mqtt.publish)
            timers.exec_()

    def configureButtons(self):
        if self.device:
            backlog = []
            buttons = ButtonsDialog(self.device)
            if buttons.exec_() == QDialog.Accepted:
                for c, cw in buttons.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                so_error = False
                for so, sow in buttons.setoption_widgets.items():
                    current_value = None
                    try:
                        current_value = self.device.setoption(so)
                    except ValueError:
                        so_error = True

                    new_value = -1

                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if not so_error and current_value and current_value != new_value:
                        backlog.append("SetOption{} {}".format(so, new_value))

                if backlog:
                    backlog.append("status 3")
                    self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def configureSwitches(self):
        if self.device:
            backlog = []
            switches = SwitchesDialog(self.device)
            if switches.exec_() == QDialog.Accepted:
                for c, cw in switches.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                so_error = False
                for so, sow in switches.setoption_widgets.items():
                    current_value = None
                    try:
                        current_value = self.device.setoption(so)
                    except ValueError:
                        so_error = True
                    new_value = -1

                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if not so_error and current_value != new_value:
                        backlog.append("SetOption{} {}".format(so, new_value))

                for sw, sw_mode in enumerate(self.device.p['SwitchMode']):
                    new_value = switches.sm.inputs[sw].currentIndex()

                    if sw_mode != new_value:
                        backlog.append("switchmode{} {}".format(sw+1, new_value))

                if backlog:
                    backlog.append("status")
                    backlog.append("status 3")
                self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def configurePower(self):
        if self.device:
            backlog = []
            power = PowerDialog(self.device)
            if power.exec_() == QDialog.Accepted:
                for c, cw in power.command_widgets.items():
                    current_value = self.device.p.get(c)
                    new_value = ""

                    if isinstance(cw.input, SpinBox):
                        new_value = cw.input.value()

                    if isinstance(cw.input, QComboBox):
                        new_value = cw.input.currentIndex()

                    if current_value != new_value:
                        backlog.append("{} {}".format(c, new_value))

                so_error = False
                for so, sow in power.setoption_widgets.items():
                    current_value = None
                    try:
                        current_value = self.device.setoption(so)
                    except ValueError:
                        so_error = True
                    new_value = -1


                    if isinstance(sow.input, SpinBox):
                        new_value = sow.input.value()

                    if isinstance(sow.input, QComboBox):
                        new_value = sow.input.currentIndex()

                    if not so_error and current_value != new_value:
                        backlog.append("SetOption{} {}".format(so, new_value))

                new_interlock_value = power.ci.input.currentData()
                new_interlock_grps = " ".join([grp.text().replace(" ", "") for grp in power.ci.groups]).rstrip()

                if new_interlock_value != self.device.p.get("Interlock", "OFF"):
                    backlog.append("interlock {}".format(new_interlock_value))

                if new_interlock_grps != self.device.p.get("Groups", ""):
                    backlog.append("interlock {}".format(new_interlock_grps))

                for i, pt in enumerate(power.cpt.inputs):
                    ptime = "PulseTime{}".format(i+1)
                    current_ptime = self.device.p.get(ptime)
                    if current_ptime:
                        current_value = list(current_ptime.keys())[0]
                        new_value = str(pt.value())

                        if new_value != current_value:
                            backlog.append("{} {}".format(ptime, new_value))

                if backlog:
                    backlog.append("status")
                    backlog.append("status 3")
                    self.mqtt.publish(self.device.cmnd_topic("backlog"), "; ".join(backlog))

    def get_dump(self):
        self.backup += self.dl.readAll()

    def save_dump(self):
        fname = self.dl.header(QNetworkRequest.ContentDispositionHeader)
        if fname:
            fname = fname.split('=')[1]
            save_file = QFileDialog.getSaveFileName(self, "Save config backup", "{}/TDM/{}".format(QDir.homePath(), fname))[0]
            if save_file:
                with open(save_file, "wb") as f:
                    f.write(self.backup)

    def check_fulltopic(self, fulltopic):
        fulltopic += "/" if not fulltopic.endswith('/') else ''
        return "%prefix%" in fulltopic and "%topic%" in fulltopic

    def closeEvent(self, event):
        event.ignore()
Exemple #52
0
    def test_qt(self):
        from PyQt5.QtCore import QTimer
        from PyQt5.QtWidgets import QApplication
        from PyQt5.QtWebEngineWidgets import QWebEnginePage
        from PyQt5.QtGui import QImageReader, QFontDatabase
        from PyQt5.QtNetwork import QNetworkAccessManager
        from calibre.utils.img import image_from_data, image_to_data, test
        # Ensure that images can be read before QApplication is constructed.
        # Note that this requires QCoreApplication.libraryPaths() to return the
        # path to the Qt plugins which it always does in the frozen build,
        # because Qt is patched to know the layout of the calibre application
        # package. On non-frozen builds, it should just work because the
        # hard-coded paths of the Qt installation should work. If they do not,
        # then it is a distro problem.
        fmts = set(
            map(lambda x: x.data().decode('utf-8'),
                QImageReader.supportedImageFormats()))  # no2to3
        testf = {'jpg', 'png', 'svg', 'ico', 'gif'}
        self.assertEqual(
            testf.intersection(fmts), testf,
            "Qt doesn't seem to be able to load some of its image plugins. Available plugins: %s"
            % fmts)
        data = P('images/blank.png', allow_user_override=False, data=True)
        img = image_from_data(data)
        image_from_data(
            P('catalog/mastheadImage.gif',
              allow_user_override=False,
              data=True))
        for fmt in 'png bmp jpeg'.split():
            d = image_to_data(img, fmt=fmt)
            image_from_data(d)
        # Run the imaging tests
        test()

        from calibre.gui2 import ensure_app, destroy_app
        display_env_var = os.environ.pop('DISPLAY', None)
        try:
            ensure_app()
            self.assertGreaterEqual(
                len(QFontDatabase().families()), 5,
                'The QPA headless plugin is not able to locate enough system fonts via fontconfig'
            )
            from calibre.ebooks.covers import create_cover
            create_cover('xxx', ['yyy'])
            na = QNetworkAccessManager()
            self.assertTrue(hasattr(na, 'sslErrors'),
                            'Qt not compiled with openssl')
            if iswindows:
                from PyQt5.Qt import QtWin
                QtWin
            p = QWebEnginePage()

            def callback(result):
                callback.result = result
                if hasattr(print_callback, 'result'):
                    QApplication.instance().quit()

            def print_callback(result):
                print_callback.result = result
                if hasattr(callback, 'result'):
                    QApplication.instance().quit()

            p.runJavaScript('1 + 1', callback)
            p.printToPdf(print_callback)
            QTimer.singleShot(5000, lambda: QApplication.instance().quit())
            QApplication.instance().exec_()
            test_flaky = ismacos and not is_ci
            if not test_flaky:
                self.assertEqual(callback.result, 2,
                                 'Simple JS computation failed')
                self.assertIn(b'Skia/PDF', bytes(print_callback.result),
                              'Print to PDF failed')
            del p
            del na
            destroy_app()
            del QWebEnginePage
        finally:
            if display_env_var is not None:
                os.environ['DISPLAY'] = display_env_var
class ProcessDialog(QDialog):
    def __init__(self, port, **kwargs):
        super().__init__()

        self.setWindowTitle('Preparando seu Satélite Educacional...')
        self.setFixedWidth(600)

        self.exception = None

        esptool.sw.progress.connect(self.update_progress)

        self.nam = QNetworkAccessManager()

        self.nrDownloads = 0

        self.nrBinFile1 = QNetworkRequest()
        self.bin_data1 = b''

        self.nrBinFile2 = QNetworkRequest()
        self.bin_data2 = b''

        self.nrBinFile3 = QNetworkRequest()
        self.bin_data3 = b''

        self.nrBinFile4 = QNetworkRequest()
        self.bin_data4 = b''

        self.setLayout(VLayout(5, 5))
        self.actions_layout = QFormLayout()
        self.actions_layout.setSpacing(5)

        self.layout().addLayout(self.actions_layout)

        self._actions = []
        self._action_widgets = {}

        self.port = port

        self.file_path = kwargs.get('file_url')
        self.file_pathBoot = kwargs.get('file_urlBoot')
        self.file_pathBootloader = kwargs.get('file_urlBootloader')
        self.file_pathPart = kwargs.get('file_urlPart')

        self._actions.append('download')

        self.erase = kwargs.get('erase')
        if self.erase:
            self._actions.append('erase')

        if self.file_path:
            self._actions.append('write')

        self.create_ui()
        self.start_process()

    def create_ui(self):
        for action in self._actions:
            pb = QProgressBar()
            pb.setFixedHeight(35)
            self._action_widgets[action] = pb
            self.actions_layout.addRow(action.capitalize(), pb)

        self.btns = QDialogButtonBox(QDialogButtonBox.Abort)
        self.btns.rejected.connect(self.abort)
        self.layout().addWidget(self.btns)

        self.sb = QStatusBar()
        self.layout().addWidget(self.sb)

    def appendBinFile1(self):
        self.bin_data1 += self.bin_reply1.readAll()

    def appendBinFile2(self):
        self.bin_data2 += self.bin_reply2.readAll()

    def appendBinFile3(self):
        self.bin_data3 += self.bin_reply3.readAll()

    def appendBinFile4(self):
        self.bin_data4 += self.bin_reply4.readAll()

    def downloadsFinished(self):
        if self.nrDownloads == 4:
            self.run_esp()

    def saveBinFile1(self):
        if self.bin_reply1.error() == QNetworkReply.NoError:
            self.file_path = self.file_path.split('/')[-1]
            with open(self.file_path, 'wb') as f:
                f.write(self.bin_data1)
            self.nrDownloads += 1
            self.downloadsFinished()
        else:
            raise NetworkError

    def saveBinFile2(self):
        if self.bin_reply2.error() == QNetworkReply.NoError:
            self.file_pathBoot = self.file_pathBoot.split('/')[-1]
            with open(self.file_pathBoot, 'wb') as f:
                f.write(self.bin_data2)
            self.nrDownloads += 1
            self.downloadsFinished()
        else:
            raise NetworkError

    def saveBinFile3(self):
        if self.bin_reply3.error() == QNetworkReply.NoError:
            self.file_pathBootloader = self.file_pathBootloader.split('/')[-1]
            with open(self.file_pathBootloader, 'wb') as f:
                f.write(self.bin_data3)
            self.nrDownloads += 1
            self.downloadsFinished()
        else:
            raise NetworkError

    def saveBinFile4(self):
        if self.bin_reply4.error() == QNetworkReply.NoError:
            self.file_pathPart = self.file_pathPart.split('/')[-1]
            with open(self.file_pathPart, 'wb') as f:
                f.write(self.bin_data4)
            self.nrDownloads += 1
            self.downloadsFinished()
        else:
            raise NetworkError

    def updateBinProgress(self, recv, total):
        self._action_widgets['download'].setValue(recv // total * 100)

    def download_bin(self):
        self.nrBinFile1.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                     True)
        self.nrBinFile1.setUrl(QUrl(self.file_path))
        self.bin_reply1 = self.nam.get(self.nrBinFile1)
        self.bin_reply1.readyRead.connect(self.appendBinFile1)
        self.bin_reply1.downloadProgress.connect(self.updateBinProgress)
        self.bin_reply1.finished.connect(self.saveBinFile1)

        self.nrBinFile2.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                     True)
        self.nrBinFile2.setUrl(QUrl(self.file_pathBoot))
        self.bin_reply2 = self.nam.get(self.nrBinFile2)
        self.bin_reply2.readyRead.connect(self.appendBinFile2)
        self.bin_reply2.finished.connect(self.saveBinFile2)

        self.nrBinFile3.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                     True)
        self.nrBinFile3.setUrl(QUrl(self.file_pathBootloader))
        self.bin_reply3 = self.nam.get(self.nrBinFile3)
        self.bin_reply3.readyRead.connect(self.appendBinFile3)
        self.bin_reply3.finished.connect(self.saveBinFile3)

        self.nrBinFile4.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
                                     True)
        self.nrBinFile4.setUrl(QUrl(self.file_pathPart))
        self.bin_reply4 = self.nam.get(self.nrBinFile4)
        self.bin_reply4.readyRead.connect(self.appendBinFile4)
        self.bin_reply4.finished.connect(self.saveBinFile4)

    def show_connection_state(self, state):
        self.sb.showMessage(state, 0)

    def run_esp(self):
        params = {
            'file_path': self.file_path,
            'file_pathBoot': self.file_pathBoot,
            'file_pathBootloader': self.file_pathBootloader,
            'file_pathPart': self.file_pathPart,
            'erase': self.erase
        }

        self.esp_thread = QThread()
        self.esp = ESPWorker(self.port, self._actions, **params)
        esptool.sw.connection_state.connect(self.show_connection_state)
        self.esp.done.connect(self.accept)
        self.esp.error.connect(self.error)
        self.esp.moveToThread(self.esp_thread)
        self.esp_thread.started.connect(self.esp.run)
        self.esp_thread.start()

    def start_process(self):
        if 'download' in self._actions:
            self.download_bin()
            self._actions = self._actions[1:]
        else:
            self.run_esp()

    def update_progress(self, action, value):
        self._action_widgets[action].setValue(value)

    @pyqtSlot()
    def stop_thread(self):
        self.esp_thread.wait(2000)
        self.esp_thread.exit()

    def accept(self):
        self.stop_thread()
        self.done(QDialog.Accepted)

    def abort(self):
        self.sb.showMessage('Aborting...', 0)
        QApplication.processEvents()
        self.esp.abort()
        self.stop_thread()
        self.reject()

    def error(self, e):
        self.exception = e
        self.abort()

    def closeEvent(self, e):
        self.stop_thread()
Exemple #54
0
class AbstractApiClient(ABC):
    """ Client for interacting with the Thingiverse API. """

    # Re-usable network manager.
    _manager = QNetworkAccessManager()

    # Prevent auto-removing running callbacks by the Python garbage collector.
    _anti_gc_callbacks = []  # type: List[Callable[[], None]]

    @abstractmethod
    def authenticate(self) -> None:
        """
        Trigger authentication flow to store user token/authorization
        """
        raise NotImplementedError("authenticate must be implemented")

    @abstractmethod
    def clearAuthentication(self) -> None:
        """
        Clear the authentication state for this driver.
        """
        raise NotImplementedError("clearAuthentication must be implemented")

    @abstractmethod
    def getThingsFromCollectionQuery(self, collection_id: str) -> str:
        """
        Get the query that returns all things in a certain collection.
        :param collection_id: The ID of the collection.
        :return: The query.
        """
        raise NotImplementedError(
            "getThingsFromCollectionQuery must be implemented")

    @abstractmethod
    def getThingsLikedByUserQuery(self) -> str:
        """
        Get the query that return all things liked by the currently authenticated user.
        :return: The query.
        """
        raise NotImplementedError(
            "getThingsLikedByUserQuery must be implemented")

    @abstractmethod
    def getThingsByUserQuery(self) -> str:
        """
        Get the query that return all things uploaded by the currently authenticated user.
        :return: The query.
        """
        raise NotImplementedError("getThingsByUserQuery must be implemented")

    @abstractmethod
    def getThingsMadeByUserQuery(self) -> str:
        """
        Get the query that return all things made by the currently authenticated user.
        :return: The query.
        """
        raise NotImplementedError(
            "getThingsMadeByUserQuery must be implemented")

    @abstractmethod
    def getPopularThingsQuery(self) -> str:
        """
        Get the query that return all popular things.
        :return: The query.
        """
        raise NotImplementedError("getPopularThingsQuery must be implemented")

    @abstractmethod
    def getFeaturedThingsQuery(self) -> str:
        """
        Get the query that return all featured things.
        :return: The query.
        """
        raise NotImplementedError("getFeaturedThingsQuery must be implemented")

    @abstractmethod
    def getNewestThingsQuery(self) -> str:
        """
        Get the query that return all newest things.
        :return: The query.
        """
        raise NotImplementedError("getNewestThingsQuery must be implemented")

    @abstractmethod
    def getThingsBySearchQuery(self, search_terms: str) -> str:
        """
        Get the query that return all things that match the given search terms.
        :return: The query.
        """
        raise NotImplementedError("getThingsBySearchQuery must be implemented")

    @abstractmethod
    def getCollections(
        self, on_finished: Callable[[List[Collection]], Any],
        on_failed: Optional[Callable[[Optional[ApiError], Optional[int]], Any]]
    ) -> None:
        """
        Get user's collections.
        :param on_finished: Callback with user's collections.
        :param on_failed: Callback with server response.
        """
        raise NotImplementedError("getCollections must be implemented")

    @abstractmethod
    def getThing(
        self,
        thing_id: int,
        on_finished: Callable[[Thing], Any],
        on_failed: Optional[Callable[[Optional[ApiError], Optional[int]],
                                     Any]] = None
    ) -> None:
        """
        Get a single thing by ID.
        :param thing_id: The thing ID.
        :param on_finished: Callback method to receive the async result on.
        :param on_failed: Callback method to receive failed request on.
        """
        raise NotImplementedError("getThing must be implemented")

    @abstractmethod
    def getThingFiles(
        self,
        thing_id: int,
        on_finished: Callable[[List[ThingFile]], Any],
        on_failed: Optional[Callable[[Optional[ApiError], Optional[int]],
                                     Any]] = None
    ) -> None:
        """
        Get a thing's files by ID.
        :param thing_id: The thing ID.
        :param on_finished: Callback method to receive the async result on.
        :param on_failed: Callback method to receive failed request on.
        """
        raise NotImplementedError("getThingFiles must be implemented")

    @abstractmethod
    def downloadThingFile(self, file_id: int, file_name: str,
                          on_finished: Callable[[bytes], Any]) -> None:
        """
        Download a thing file by its ID.
        :param file_id: The file ID to download.
        :param file_name: The file's name including extension.
        :param on_finished: Callback method to receive the async result on as bytes.
        """
        raise NotImplementedError("downloadThingFile must be implemented")

    @abstractmethod
    def getThings(
        self,
        query: str,
        page: int,
        on_finished: Callable[[List[Thing]], Any],
        on_failed: Optional[Callable[[Optional[ApiError], Optional[int]],
                                     Any]] = None
    ) -> None:
        """
        Get things by query.
        :param query: The things to get.
        :param page: Page number of query results (for pagination).
        :param on_finished: Callback method to receive the async result on.
        :param on_failed: Callback method to receive failed request on.
        """
        raise NotImplementedError("get must be implemented")

    @property
    @abstractmethod
    def _root_url(self) -> str:
        """
        Get the API root URL for this provider.
        :returns: The root URL as string.
        """
        raise NotImplementedError("_root_url must be implemented")

    @abstractmethod
    def _setAuth(self, request: QNetworkRequest):
        """
        Get the API authentication method for this provider.
        """
        raise NotImplementedError("_setAuth must be implemented")

    def _createEmptyRequest(
            self,
            url: str,
            content_type: str = "application/json") -> QNetworkRequest:
        """
        Create a new network request with the needed HTTP headers.
        :param url: The full URL to do the request on.
        :param content_type: Content-Type header value
        :return: The QNetworkRequest.
        """
        request = QNetworkRequest(QUrl(url))
        request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
        request.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
                             True)  # file downloads reply with a 302 first
        self._setAuth(request)
        return request

    def _addCallback(
        self,
        reply: QNetworkReply,
        on_finished: Callable[[Any], Any],
        on_failed: Optional[Callable[[Optional[ApiError], Optional[int]],
                                     Any]] = None,
        parser: Optional[Callable[[QNetworkReply], Tuple[int, Any]]] = None
    ) -> None:
        """
        Creates a callback function so that it includes the parsing of the response into the correct model.
        The callback is added to the 'finished' signal of the reply.
        :param reply: The reply that should be listened to.
        :param on_finished: The callback in case the request is successful.
        :param on_failed: The callback in case the request fails.
        :param parser: A custom parser for the response data, defaults to a JSON parser.
        """
        def parse() -> None:
            self._anti_gc_callbacks.remove(parse)
            status_code, response = parser(
                reply) if parser else ApiHelper.parseReplyAsJson(reply)
            if not status_code or status_code >= 400 or response is None:
                Logger.warning(
                    "API returned with status {} and body {}".format(
                        status_code, response))
                if on_failed:
                    error_response = None
                    if isinstance(response, dict):
                        error_response = ApiError(response)
                    on_failed(error_response, status_code)
            else:
                on_finished(response)
            reply.deleteLater()

        self._anti_gc_callbacks.append(parse)
        reply.finished.connect(parse)  # type: ignore
Exemple #55
0
 def __init__(self, *args, **kwargs):
     super(UrlSchemeHandler, self).__init__(*args, **kwargs)
     self._manager = QNetworkAccessManager(self)
     self._manager.finished.connect(self.onFinished)
Exemple #56
0
class DiscoverOctoPrintAction(MachineAction):
    def __init__(self, parent: QObject = None) -> None:
        super().__init__("DiscoverOctoPrintAction",
                         catalog.i18nc("@action", "Connect OctoPrint"))

        self._qml_url = os.path.join("qml", "DiscoverOctoPrintAction.qml")

        self._application = CuraApplication.getInstance()
        self._network_plugin = None  # type: Optional[OctoPrintOutputDevicePlugin]

        #   QNetwork manager needs to be created in advance. If we don't it can happen that it doesn't correctly
        #   hook itself into the event loop, which results in events never being fired / done.
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onRequestFinished)

        self._settings_reply = None  # type: Optional[QNetworkReply]
        self._settings_reply_timeout = None  # type: Optional[NetworkReplyTimeout]

        self._instance_supports_appkeys = False
        self._appkey_reply = None  # type: Optional[QNetworkReply]
        self._appkey_request = None  # type: Optional[QNetworkRequest]
        self._appkey_instance_id = ""

        self._appkey_poll_timer = QTimer()
        self._appkey_poll_timer.setInterval(500)
        self._appkey_poll_timer.setSingleShot(True)
        self._appkey_poll_timer.timeout.connect(self._pollApiKey)

        # Try to get version information from plugin.json
        plugin_file_path = os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "plugin.json")
        try:
            with open(plugin_file_path) as plugin_file:
                plugin_info = json.load(plugin_file)
                self._plugin_version = plugin_info["version"]
        except:
            # The actual version info is not critical to have so we can continue
            self._plugin_version = "0.0"
            Logger.logException(
                "w", "Could not get version information for the plugin")

        self._user_agent = ("%s/%s %s/%s" %
                            (self._application.getApplicationName(),
                             self._application.getVersion(), "OctoPrintPlugin",
                             self._plugin_version)).encode()

        self._settings_instance = None  # type: Optional[OctoPrintOutputDevice]

        self._instance_responded = False
        self._instance_in_error = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self._instance_installed_plugins = []  # type: List[str]

        self._power_plugins_manager = OctoPrintPowerPlugins()

        # Load keys cache from preferences
        self._preferences = self._application.getPreferences()
        self._preferences.addPreference("octoprint/keys_cache", "")

        try:
            self._keys_cache = json.loads(
                self._deobfuscateString(
                    self._preferences.getValue("octoprint/keys_cache")))
        except ValueError:
            self._keys_cache = {}  # type: Dict[str, Any]
        if not isinstance(self._keys_cache, dict):
            self._keys_cache = {}  # type: Dict[str, Any]

        self._additional_components = None  # type:Optional[QObject]

        ContainerRegistry.getInstance().containerAdded.connect(
            self._onContainerAdded)
        self._application.engineCreatedSignal.connect(
            self._createAdditionalComponentsView)

    @pyqtProperty(str, constant=True)
    def pluginVersion(self) -> str:
        return self._plugin_version

    @pyqtSlot()
    def startDiscovery(self) -> None:
        if not self._plugin_id:
            return
        if not self._network_plugin:
            self._network_plugin = cast(
                OctoPrintOutputDevicePlugin,
                self._application.getOutputDeviceManager().
                getOutputDevicePlugin(self._plugin_id))
            if not self._network_plugin:
                return
            self._network_plugin.addInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.removeInstanceSignal.connect(
                self._onInstanceDiscovery)
            self._network_plugin.instanceListChanged.connect(
                self._onInstanceDiscovery)
            self.instancesChanged.emit()
        else:
            # Restart bonjour discovery
            self._network_plugin.startDiscovery()

    def _onInstanceDiscovery(self, *args) -> None:
        self.instancesChanged.emit()

    @pyqtSlot(str)
    def removeManualInstance(self, name: str) -> None:
        if not self._network_plugin:
            return

        self._network_plugin.removeManualInstance(name)

    @pyqtSlot(str, str, int, str, bool, str, str)
    def setManualInstance(self,
                          name: str,
                          address: str,
                          port: int,
                          path: str,
                          useHttps: bool,
                          userName: str = "",
                          password: str = "") -> None:
        if not self._network_plugin:
            return

        # This manual printer could replace a current manual printer
        self._network_plugin.removeManualInstance(name)

        self._network_plugin.addManualInstance(name, address, port, path,
                                               useHttps, userName, password)

    def _onContainerAdded(self, container: "ContainerInterface") -> None:
        # Add this action as a supported action to all machine definitions
        if (isinstance(container, DefinitionContainer)
                and container.getMetaDataEntry("type") == "machine"
                and container.getMetaDataEntry("supports_usb_connection")):

            self._application.getMachineActionManager().addSupportedAction(
                container.getId(), self.getKey())

    instancesChanged = pyqtSignal()
    appKeysSupportedChanged = pyqtSignal()
    appKeyReceived = pyqtSignal()
    instanceIdChanged = pyqtSignal()

    @pyqtProperty("QVariantList", notify=instancesChanged)
    def discoveredInstances(self) -> List[Any]:
        if self._network_plugin:
            instances = list(self._network_plugin.getInstances().values())
            instances.sort(key=lambda k: k.name)
            return instances
        else:
            return []

    @pyqtSlot(str)
    def setInstanceId(self, key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if global_container_stack:
            global_container_stack.setMetaDataEntry("octoprint_id", key)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

        self.instanceIdChanged.emit()

    @pyqtProperty(str, notify=instanceIdChanged)
    def instanceId(self) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        return global_container_stack.getMetaDataEntry("octoprint_id", "")

    @pyqtSlot(str)
    def requestApiKey(self, instance_id: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url:
            return

        ## Request appkey
        self._appkey_instance_id = instance_id
        self._appkey_request = self._createRequest(
            QUrl(base_url + "plugin/appkeys/request"), basic_auth_username,
            basic_auth_password)
        self._appkey_request.setRawHeader(b"Content-Type", b"application/json")
        data = json.dumps({"app": "Cura"})
        self._appkey_reply = self._network_manager.post(
            self._appkey_request, data.encode())

    @pyqtSlot()
    def cancelApiKeyRequest(self) -> None:
        if self._appkey_reply:
            if self._appkey_reply.isRunning():
                self._appkey_reply.abort()
            self._appkey_reply = None

        self._appkey_request = None  # type: Optional[QNetworkRequest]

        self._appkey_poll_timer.stop()

    def _pollApiKey(self) -> None:
        if not self._appkey_request:
            return
        self._appkey_reply = self._network_manager.get(self._appkey_request)

    @pyqtSlot(str)
    def probeAppKeySupport(self, instance_id: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url or not instance:
            return

        instance.getAdditionalData()

        self._instance_supports_appkeys = False
        self.appKeysSupportedChanged.emit()

        appkey_probe_request = self._createRequest(
            QUrl(base_url + "plugin/appkeys/probe"), basic_auth_username,
            basic_auth_password)
        self._appkey_reply = self._network_manager.get(appkey_probe_request)

    @pyqtSlot(str, str)
    def testApiKey(self, instance_id: str, api_key: str) -> None:
        (instance, base_url, basic_auth_username,
         basic_auth_password) = self._getInstanceInfo(instance_id)
        if not base_url:
            return

        self._instance_responded = False
        self._instance_api_key_accepted = False
        self._instance_supports_sd = False
        self._instance_supports_camera = False
        self._instance_installed_plugins = []  # type: List[str]
        self.selectedInstanceSettingsChanged.emit()

        if self._settings_reply:
            if self._settings_reply.isRunning():
                self._settings_reply.abort()
            self._settings_reply = None
        if self._settings_reply_timeout:
            self._settings_reply_timeout = None

        if api_key != "":
            Logger.log(
                "d",
                "Trying to access OctoPrint instance at %s with the provided API key."
                % base_url)

            ## Request 'settings' dump
            settings_request = self._createRequest(
                QUrl(base_url + "api/settings"), basic_auth_username,
                basic_auth_password)
            settings_request.setRawHeader(b"X-Api-Key", api_key.encode())
            self._settings_reply = self._network_manager.get(settings_request)
            self._settings_reply_timeout = NetworkReplyTimeout(
                self._settings_reply, 5000, self._onRequestFailed)

            self._settings_instance = instance

    @pyqtSlot(str)
    def setApiKey(self, api_key: str) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        global_container_stack.setMetaDataEntry(
            "octoprint_api_key",
            base64.b64encode(api_key.encode("ascii")).decode("ascii"))

        self._keys_cache[self.instanceId] = api_key
        keys_cache = base64.b64encode(
            json.dumps(self._keys_cache).encode("ascii")).decode("ascii")
        self._preferences.setValue("octoprint/keys_cache", keys_cache)

        if self._network_plugin:
            # Ensure that the connection states are refreshed.
            self._network_plugin.reCheckConnections()

    ##  Get the stored API key of an instance, or the one stored in the machine instance
    #   \return key String containing the key of the machine.
    @pyqtSlot(str, result=str)
    def getApiKey(self, instance_id: str) -> str:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return ""

        if instance_id == self.instanceId:
            api_key = self._deobfuscateString(
                global_container_stack.getMetaDataEntry(
                    "octoprint_api_key", ""))
        else:
            api_key = self._keys_cache.get(instance_id, "")

        return api_key

    selectedInstanceSettingsChanged = pyqtSignal()

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceResponded(self) -> bool:
        return self._instance_responded

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceInError(self) -> bool:
        return self._instance_in_error

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceApiKeyAccepted(self) -> bool:
        return self._instance_api_key_accepted

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsSd(self) -> bool:
        return self._instance_supports_sd

    @pyqtProperty(bool, notify=selectedInstanceSettingsChanged)
    def instanceSupportsCamera(self) -> bool:
        return self._instance_supports_camera

    @pyqtProperty("QStringList", notify=selectedInstanceSettingsChanged)
    def instanceInstalledPlugins(self) -> List[str]:
        return self._instance_installed_plugins

    @pyqtProperty("QVariantList", notify=selectedInstanceSettingsChanged)
    def instanceAvailablePowerPlugins(self) -> List[Dict[str, str]]:
        available_plugins = self._power_plugins_manager.getAvailablePowerPlugs(
        )
        return [{
            "key": plug_id,
            "text": plug_data["name"]
        } for (plug_id, plug_data) in available_plugins.items()]

    @pyqtProperty(bool, notify=appKeysSupportedChanged)
    def instanceSupportsAppKeys(self) -> bool:
        return self._instance_supports_appkeys

    @pyqtSlot(str, str, str)
    def setContainerMetaDataEntry(self, container_id: str, key: str,
                                  value: str) -> None:
        containers = ContainerRegistry.getInstance().findContainers(
            id=container_id)
        if not containers:
            Logger.log(
                "w",
                "Could not set metadata of container %s because it was not found.",
                container_id)
            return

        containers[0].setMetaDataEntry(key, value)

    @pyqtSlot(bool)
    def applyGcodeFlavorFix(self, apply_fix: bool) -> None:
        global_container_stack = self._application.getGlobalContainerStack()
        if not global_container_stack:
            return

        gcode_flavor = "RepRap (Marlin/Sprinter)" if apply_fix else "UltiGCode"
        if global_container_stack.getProperty("machine_gcode_flavor",
                                              "value") == gcode_flavor:
            # No need to add a definition_changes container if the setting is not going to be changed
            return

        # Make sure there is a definition_changes container to store the machine settings
        definition_changes_container = global_container_stack.definitionChanges
        if definition_changes_container == ContainerRegistry.getInstance(
        ).getEmptyInstanceContainer():
            definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
                global_container_stack,
                global_container_stack.getId() + "_settings")

        definition_changes_container.setProperty("machine_gcode_flavor",
                                                 "value", gcode_flavor)

        # Update the has_materials metadata flag after switching gcode flavor
        definition = global_container_stack.getBottom()
        if (not definition or definition.getProperty("machine_gcode_flavor",
                                                     "value") != "UltiGCode"
                or definition.getMetaDataEntry("has_materials", False)):

            # In other words: only continue for the UM2 (extended), but not for the UM2+
            return

        has_materials = global_container_stack.getProperty(
            "machine_gcode_flavor", "value") != "UltiGCode"

        material_container = global_container_stack.material

        if has_materials:
            global_container_stack.setMetaDataEntry("has_materials", True)

            # Set the material container to a sane default
            if material_container == ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer():
                search_criteria = {
                    "type":
                    "material",
                    "definition":
                    "fdmprinter",
                    "id":
                    global_container_stack.getMetaDataEntry(
                        "preferred_material")
                }
                materials = ContainerRegistry.getInstance(
                ).findInstanceContainers(**search_criteria)
                if materials:
                    global_container_stack.material = materials[0]
        else:
            # The metadata entry is stored in an ini, and ini files are parsed as strings only.
            # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
            if "has_materials" in global_container_stack.getMetaData():
                global_container_stack.removeMetaDataEntry("has_materials")

            global_container_stack.material = ContainerRegistry.getInstance(
            ).getEmptyInstanceContainer()

        self._application.globalContainerStackChanged.emit()

    @pyqtSlot(str)
    def openWebPage(self, url: str) -> None:
        QDesktopServices.openUrl(QUrl(url))

    def _createAdditionalComponentsView(self) -> None:
        Logger.log(
            "d",
            "Creating additional ui components for OctoPrint-connected printers."
        )

        path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "qml",
                            "OctoPrintComponents.qml")
        self._additional_components = self._application.createQmlComponent(
            path, {"manager": self})
        if not self._additional_components:
            Logger.log(
                "w",
                "Could not create additional components for OctoPrint-connected printers."
            )
            return

        self._application.addAdditionalComponent(
            "monitorButtons",
            self._additional_components.findChild(QObject,
                                                  "openOctoPrintButton"))

    def _onRequestFailed(self, reply: QNetworkReply) -> None:
        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                Logger.log(
                    "w",
                    "Connection refused or timeout when trying to access OctoPrint at %s"
                    % reply.url().toString())
                self._instance_in_error = True
                self.selectedInstanceSettingsChanged.emit()

    ##  Handler for all requests that have finished.
    def _onRequestFinished(self, reply: QNetworkReply) -> None:

        http_status_code = reply.attribute(
            QNetworkRequest.HttpStatusCodeAttribute)
        if not http_status_code:
            # Received no or empty reply
            self._onRequestFailed(reply)
            return

        if reply.operation() == QNetworkAccessManager.PostOperation:
            if "/plugin/appkeys/request" in reply.url().toString(
            ):  # Initial AppKey request
                if http_status_code == 201 or http_status_code == 202:
                    Logger.log("w", "Start polling for AppKeys decision")
                    if not self._appkey_request:
                        return
                    self._appkey_request.setUrl(
                        reply.header(QNetworkRequest.LocationHeader))
                    self._appkey_request.setRawHeader(b"Content-Type", b"")
                    self._appkey_poll_timer.start()
                elif http_status_code == 404:
                    Logger.log(
                        "w",
                        "This instance of OctoPrint does not support AppKeys")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log(
                        "w",
                        "Unknown response when requesting an AppKey: %d. OctoPrint said %s"
                        % (http_status_code, response))
                    self._appkey_request = None  # type: Optional[QNetworkRequest]

        if reply.operation() == QNetworkAccessManager.GetOperation:
            if "/plugin/appkeys/probe" in reply.url().toString(
            ):  # Probe for AppKey support
                if http_status_code == 204:
                    self._instance_supports_appkeys = True
                else:
                    self._instance_supports_appkeys = False
                self.appKeysSupportedChanged.emit()

            if "/plugin/appkeys/request" in reply.url().toString(
            ):  # Periodic AppKey request poll
                if http_status_code == 202:
                    self._appkey_poll_timer.start()
                elif http_status_code == 200:
                    Logger.log("d", "AppKey granted")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        return

                    api_key = json_data["api_key"]
                    self._keys_cache[self._appkey_instance_id] = api_key

                    global_container_stack = self._application.getGlobalContainerStack(
                    )
                    if global_container_stack:
                        global_container_stack.setMetaDataEntry(
                            "octoprint_api_key",
                            base64.b64encode(
                                api_key.encode("ascii")).decode("ascii"))

                    self.appKeyReceived.emit()
                elif http_status_code == 404:
                    Logger.log("d", "AppKey denied")
                    self._appkey_request = None  # type: Optional[QNetworkRequest]
                else:
                    response = bytes(reply.readAll()).decode()
                    Logger.log(
                        "w",
                        "Unknown response when waiting for an AppKey: %d. OctoPrint said %s"
                        % (http_status_code, response))
                    self._appkey_request = None  # type: Optional[QNetworkRequest]

            if "api/settings" in reply.url().toString(
            ):  # OctoPrint settings dump from /settings:
                self._instance_in_error = False

                if http_status_code == 200:
                    Logger.log("d", "API key accepted by OctoPrint.")
                    self._instance_api_key_accepted = True

                    try:
                        json_data = json.loads(
                            bytes(reply.readAll()).decode("utf-8"))
                    except json.decoder.JSONDecodeError:
                        Logger.log(
                            "w",
                            "Received invalid JSON from octoprint instance.")
                        json_data = {}

                    if "feature" in json_data and "sdSupport" in json_data[
                            "feature"]:
                        self._instance_supports_sd = json_data["feature"][
                            "sdSupport"]

                    if "webcam" in json_data and "streamUrl" in json_data[
                            "webcam"]:
                        stream_url = json_data["webcam"]["streamUrl"]
                        if stream_url:  #not empty string or None
                            self._instance_supports_camera = True

                    if "plugins" in json_data:
                        self._power_plugins_manager.parsePluginData(
                            json_data["plugins"])
                        self._instance_installed_plugins = list(
                            json_data["plugins"].keys())

                    api_key = bytes(reply.request().rawHeader(
                        b"X-Api-Key")).decode("utf-8")
                    self.setApiKey(api_key)  # store api key in key cache
                    if self._settings_instance:
                        self._settings_instance.setApiKey(api_key)
                        self._settings_instance.resetOctoPrintUserName()
                        self._settings_instance.getAdditionalData()
                        self._settings_instance.parseSettingsData(json_data)

                    self._settings_instance = None

                elif http_status_code == 401:
                    Logger.log("d", "Invalid API key for OctoPrint.")
                    self._instance_api_key_accepted = False

                elif http_status_code == 502 or http_status_code == 503:
                    Logger.log("d", "OctoPrint is not running.")
                    self._instance_api_key_accepted = False
                    self._instance_in_error = True

                self._instance_responded = True
                self.selectedInstanceSettingsChanged.emit()

    def _createRequest(self,
                       url: str,
                       basic_auth_username: str = "",
                       basic_auth_password: str = "") -> QNetworkRequest:
        request = QNetworkRequest(url)
        request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
        request.setRawHeader(b"User-Agent", self._user_agent)

        if basic_auth_username and basic_auth_password:
            data = base64.b64encode(
                ("%s:%s" % (basic_auth_username,
                            basic_auth_password)).encode()).decode("utf-8")
            request.setRawHeader(b"Authorization",
                                 ("Basic %s" % data).encode())

        # ignore SSL errors (eg for self-signed certificates)
        ssl_configuration = QSslConfiguration.defaultConfiguration()
        ssl_configuration.setPeerVerifyMode(QSslSocket.VerifyNone)
        request.setSslConfiguration(ssl_configuration)

        return request

    ##  Utility handler to base64-decode a string (eg an obfuscated API key), if it has been encoded before
    def _deobfuscateString(self, source: str) -> str:
        try:
            return base64.b64decode(source.encode("ascii")).decode("ascii")
        except UnicodeDecodeError:
            return source

    def _getInstanceInfo(
        self, instance_id: str
    ) -> Tuple[Optional[OctoPrintOutputDevice], str, str, str]:
        if not self._network_plugin:
            return (None, "", "", "")
        instance = self._network_plugin.getInstanceById(instance_id)
        if not instance:
            return (None, "", "", "")

        return (instance, instance.baseURL, instance.getProperty("userName"),
                instance.getProperty("password"))
Exemple #57
0
 def downloadPortraits(self, user):
     #ToDo: finalize this
     netwManager = QNetworkAccessManager()
     url = QUrl()
Exemple #58
0
class PACFetcher(QObject):

    """Asynchronous fetcher of PAC files."""

    finished = pyqtSignal()

    def __init__(self, url, parent=None):
        """Resolve a PAC proxy from URL.

        Args:
            url: QUrl of a PAC proxy.
        """
        super().__init__(parent)

        pac_prefix = "pac+"

        assert url.scheme().startswith(pac_prefix)
        url.setScheme(url.scheme()[len(pac_prefix):])

        self._pac_url = url
        self._manager = QNetworkAccessManager()
        self._manager.setProxy(QNetworkProxy(QNetworkProxy.NoProxy))
        self._pac = None
        self._error_message = None
        self._reply = None

    def __eq__(self, other):
        # pylint: disable=protected-access
        return self._pac_url == other._pac_url

    def __repr__(self):
        return utils.get_repr(self, url=self._pac_url, constructor=True)

    def fetch(self):
        """Fetch the proxy from the remote URL."""
        self._reply = self._manager.get(QNetworkRequest(self._pac_url))
        self._reply.finished.connect(self._finish)

    @pyqtSlot()
    def _finish(self):
        if self._reply.error() != QNetworkReply.NoError:
            error = "Can't fetch PAC file from URL, error code {}: {}"
            self._error_message = error.format(
                self._reply.error(), self._reply.errorString())
            log.network.error(self._error_message)
        else:
            try:
                pacscript = bytes(self._reply.readAll()).decode("utf-8")
            except UnicodeError as e:
                error = "Invalid encoding of a PAC file: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
            try:
                self._pac = PACResolver(pacscript)
                log.network.debug("Successfully evaluated PAC file.")
            except EvalProxyError as e:
                error = "Error in PAC evaluation: {}"
                self._error_message = error.format(e)
                log.network.exception(self._error_message)
        self._manager = None
        self._reply = None
        self.finished.emit()

    def _wait(self):
        """Wait until a reply from the remote server is received."""
        if self._manager is not None:
            loop = qtutils.EventLoop()
            self.finished.connect(loop.quit)
            loop.exec_()

    def fetch_error(self):
        """Check if PAC script is successfully fetched.

        Return None iff PAC script is downloaded and evaluated successfully,
        error string otherwise.
        """
        self._wait()
        return self._error_message

    def resolve(self, query):
        """Resolve a query via PAC.

        Args: QNetworkProxyQuery.

        Return a list of QNetworkProxy objects in order of preference.
        """
        self._wait()
        from_file = self._pac_url.scheme() == 'file'
        try:
            return self._pac.resolve(query, from_file=from_file)
        except (EvalProxyError, ParseProxyError) as e:
            log.network.exception("Error in PAC resolution: {}.".format(e))
            # .invalid is guaranteed to be inaccessible in RFC 6761.
            # Port 9 is for DISCARD protocol -- DISCARD servers act like
            # /dev/null.
            # Later NetworkManager.createRequest will detect this and display
            # an error message.
            error_host = "pac-resolve-error.qutebrowser.invalid"
            return [QNetworkProxy(QNetworkProxy.HttpProxy, error_host, 9)]
Exemple #59
0
class UM3OutputDevicePlugin(OutputDevicePlugin):
    addDeviceSignal = Signal()
    removeDeviceSignal = Signal()
    discoveredDevicesChanged = Signal()

    def __init__(self):
        super().__init__()
        self._zero_conf = None
        self._zero_conf_browser = None

        # Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
        self.addDeviceSignal.connect(self._onAddDevice)
        self.removeDeviceSignal.connect(self._onRemoveDevice)

        Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)

        self._discovered_devices = {}
        
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self._onNetworkRequestFinished)

        self._min_cluster_version = Version("4.0.0")

        self._api_version = "1"
        self._api_prefix = "/api/v" + self._api_version + "/"
        self._cluster_api_version = "1"
        self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"

        # Get list of manual instances from preferences
        self._preferences = Preferences.getInstance()
        self._preferences.addPreference("um3networkprinting/manual_instances",
                                        "")  # A comma-separated list of ip adresses or hostnames

        self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")

        # The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
        # which fail to get detailed service info.
        # Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
        # them up and process them.
        self._service_changed_request_queue = Queue()
        self._service_changed_request_event = Event()
        self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
        self._service_changed_request_thread.start()

    def getDiscoveredDevices(self):
        return self._discovered_devices

    ##  Start looking for devices on network.
    def start(self):
        self.startDiscovery()

    def startDiscovery(self):
        self.stop()
        if self._zero_conf_browser:
            self._zero_conf_browser.cancel()
            self._zero_conf_browser = None  # Force the old ServiceBrowser to be destroyed.

        for instance_name in list(self._discovered_devices):
            self._onRemoveDevice(instance_name)

        self._zero_conf = Zeroconf()
        self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
                                                 [self._appendServiceChangedRequest])

        # Look for manual instances from preference
        for address in self._manual_instances:
            if address:
                self.addManualDevice(address)

    def reCheckConnections(self):
        active_machine = Application.getInstance().getGlobalContainerStack()
        if not active_machine:
            return

        um_network_key = active_machine.getMetaDataEntry("um_network_key")

        for key in self._discovered_devices:
            if key == um_network_key:
                if not self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to connect with [%s]" % key)
                    self._discovered_devices[key].connect()
                    self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
            else:
                if self._discovered_devices[key].isConnected():
                    Logger.log("d", "Attempting to close connection with [%s]" % key)
                    self._discovered_devices[key].close()
                    self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)

    def _onDeviceConnectionStateChanged(self, key):
        if key not in self._discovered_devices:
            return
        if self._discovered_devices[key].isConnected():
            self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
        else:
            self.getOutputDeviceManager().removeOutputDevice(key)

    def stop(self):
        if self._zero_conf is not None:
            Logger.log("d", "zeroconf close...")
            self._zero_conf.close()

    def removeManualDevice(self, key, address = None):
        if key in self._discovered_devices:
            if not address:
                address = self._discovered_devices[key].ipAddress
            self._onRemoveDevice(key)

        if address in self._manual_instances:
            self._manual_instances.remove(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

    def addManualDevice(self, address):
        if address not in self._manual_instances:
            self._manual_instances.append(address)
            self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))

        instance_name = "manual:%s" % address
        properties = {
            b"name": address.encode("utf-8"),
            b"address": address.encode("utf-8"),
            b"manual": b"true",
            b"incomplete": b"true"
        }

        if instance_name not in self._discovered_devices:
            # Add a preliminary printer instance
            self._onAddDevice(instance_name, address, properties)

        self._checkManualDevice(address)

    def _checkManualDevice(self, address):
        # Check if a UM3 family device exists at this address.
        # If a printer responds, it will replace the preliminary printer created above
        # origin=manual is for tracking back the origin of the call
        url = QUrl("http://" + address + self._api_prefix + "system")
        name_request = QNetworkRequest(url)
        self._network_manager.get(name_request)

    def _onNetworkRequestFinished(self, reply):
        reply_url = reply.url().toString()

        if "system" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the firmware version!
                return

            try:
                system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return

            address = reply.url().host()
            has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
            instance_name = "manual:%s" % address
            properties = {
                b"name": system_info["name"].encode("utf-8"),
                b"address": address.encode("utf-8"),
                b"firmware_version": system_info["firmware"].encode("utf-8"),
                b"manual": b"true",
                b"machine": system_info["variant"].encode("utf-8")
            }

            if has_cluster_capable_firmware:
                # Cluster needs an additional request, before it's completed.
                properties[b"incomplete"] = b"true"

            # Check if the device is still in the list & re-add it with the updated
            # information.
            if instance_name in self._discovered_devices:
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

            if has_cluster_capable_firmware:
                # We need to request more info in order to figure out the size of the cluster.
                cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
                cluster_request = QNetworkRequest(cluster_url)
                self._network_manager.get(cluster_request)

        elif "printers" in reply_url:
            if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
                # Something went wrong with checking the amount of printers the cluster has!
                return
            # So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
            try:
                cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
            except:
                Logger.log("e", "Something went wrong converting the JSON.")
                return
            address = reply.url().host()
            instance_name = "manual:%s" % address
            if instance_name in self._discovered_devices:
                device = self._discovered_devices[instance_name]
                properties = device.getProperties().copy()
                if b"incomplete" in properties:
                    del properties[b"incomplete"]
                properties[b'cluster_size'] = len(cluster_printers_list)
                self._onRemoveDevice(instance_name)
                self._onAddDevice(instance_name, address, properties)

    def _onRemoveDevice(self, device_id):
        device = self._discovered_devices.pop(device_id, None)
        if device:
            if device.isConnected():
                device.disconnect()
                try:
                    device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
                except TypeError:
                    # Disconnect already happened.
                    pass

            self.discoveredDevicesChanged.emit()

    def _onAddDevice(self, name, address, properties):
        # Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
        # or "Legacy" UM3 device.
        cluster_size = int(properties.get(b"cluster_size", -1))

        if cluster_size >= 0:
            device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
        else:
            device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)

        self._discovered_devices[device.getId()] = device
        self.discoveredDevicesChanged.emit()

        global_container_stack = Application.getInstance().getGlobalContainerStack()
        if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
            device.connect()
            device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)

    ##  Appends a service changed request so later the handling thread will pick it up and processes it.
    def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
        # append the request and set the event so the event handling thread can pick it up
        item = (zeroconf, service_type, name, state_change)
        self._service_changed_request_queue.put(item)
        self._service_changed_request_event.set()

    def _handleOnServiceChangedRequests(self):
        while True:
            # Wait for the event to be set
            self._service_changed_request_event.wait(timeout = 5.0)

            # Stop if the application is shutting down
            if Application.getInstance().isShuttingDown():
                return

            self._service_changed_request_event.clear()

            # Handle all pending requests
            reschedule_requests = []  # A list of requests that have failed so later they will get re-scheduled
            while not self._service_changed_request_queue.empty():
                request = self._service_changed_request_queue.get()
                zeroconf, service_type, name, state_change = request
                try:
                    result = self._onServiceChanged(zeroconf, service_type, name, state_change)
                    if not result:
                        reschedule_requests.append(request)
                except Exception:
                    Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
                                        service_type, name)
                    reschedule_requests.append(request)

            # Re-schedule the failed requests if any
            if reschedule_requests:
                for request in reschedule_requests:
                    self._service_changed_request_queue.put(request)

    ##  Handler for zeroConf detection.
    #   Return True or False indicating if the process succeeded.
    #   Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
    def _onServiceChanged(self, zero_conf, service_type, name, state_change):
        if state_change == ServiceStateChange.Added:
            Logger.log("d", "Bonjour service added: %s" % name)

            # First try getting info from zero-conf cache
            info = ServiceInfo(service_type, name, properties={})
            for record in zero_conf.cache.entries_with_name(name.lower()):
                info.update_record(zero_conf, time(), record)

            for record in zero_conf.cache.entries_with_name(info.server):
                info.update_record(zero_conf, time(), record)
                if info.address:
                    break

            # Request more data if info is not complete
            if not info.address:
                Logger.log("d", "Trying to get address of %s", name)
                info = zero_conf.get_service_info(service_type, name)

            if info:
                type_of_device = info.properties.get(b"type", None)
                if type_of_device:
                    if type_of_device == b"printer":
                        address = '.'.join(map(lambda n: str(n), info.address))
                        self.addDeviceSignal.emit(str(name), address, info.properties)
                    else:
                        Logger.log("w",
                                   "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
            else:
                Logger.log("w", "Could not get information about %s" % name)
                return False

        elif state_change == ServiceStateChange.Removed:
            Logger.log("d", "Bonjour service removed: %s" % name)
            self.removeDeviceSignal.emit(str(name))

        return True
Exemple #60
-5
    def __init__(self, argv, qapp):
        '''
        Create a new "cutecoin" application

        :param argv: The argv parameters of the call
        '''
        super().__init__()
        self.accounts = {}
        self.current_account = None
        self.monitor = None
        self.available_version = (True,
                                  __version__,
                                  "")
        config.parse_arguments(argv)
        self._network_manager = QNetworkAccessManager()
        self._network_manager.finished.connect(self.read_available_version)
        self.preferences = {'account': "",
                            'lang': 'en_GB',
                            'ref': 0
                            }

        self.load()

        translator = QTranslator(qapp)
        logging.debug("Loading translations")
        locale = self.preferences['lang']
        QLocale.setDefault(QLocale(locale))
        if translator.load(":/i18n/{0}".format(locale)):
            if QCoreApplication.installTranslator(translator):
                logging.debug("Loaded i18n/{0}".format(locale))
        else:
            logging.debug("Couldn't load translation")