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)
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)
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()
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(",")
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 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_()
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))
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]]
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)
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)
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)
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
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
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)
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 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()
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()
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()
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())])
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)
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(' ', ' ') newstring = newstring.replace('&', '&') 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
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]
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()
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)
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))
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"))
def __init__(self, url): QNetworkAccessManager.__init__(self) self.request = QNetworkRequest(QUrl(url)) self.reply = self.get(self.request)
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))
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()
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()
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()
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())
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)
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()
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()
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
def __init__(self, *args, **kwargs): super(UrlSchemeHandler, self).__init__(*args, **kwargs) self._manager = QNetworkAccessManager(self) self._manager.finished.connect(self.onFinished)
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"))
def downloadPortraits(self, user): #ToDo: finalize this netwManager = QNetworkAccessManager() url = QUrl()
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)]
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
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")