Source code for nxbrew_dl.gui.gui_nxbrew_dl

import os
import sys
import time
import traceback
from functools import partial
from urllib.parse import urlparse

import requests
from PySide6.QtCore import (
    Slot,
    Signal,
    QObject,
    QThread,
    QSize,
    Qt,
)
from PySide6.QtGui import QIcon
from PySide6.QtWidgets import (
    QMessageBox,
    QMainWindow,
    QFileDialog,
)
from myjdapi.exception import MYJDException
from packaging.version import Version

import nxbrew_dl
from .gui_about import AboutWindow
from .gui_regions_languages import RegionLanguageWindow
from .gui_utils import (
    open_url,
    add_row_to_table,
    get_ordered_list,
)
from .layout_nxbrew_dl import Ui_nxbrew_dl
from ..nxbrew_dl import NXBrew
from ..util import (
    check_github_version,
    get_game_dict,
    NXBrewLogger,
    load_yml,
    save_yml,
    load_json,
)


def open_game_url(item):
    """If a row title is clicked, open the associated URL"""

    column = item.column()

    # If we're not clicking the name, don't do anything
    if column != 0:
        return

    # Search by URL, so pull that out here
    url = item.toolTip()
    open_url(url)


[docs] class MainWindow(QMainWindow): def __init__(self): """NXBrew-dl Main Window This is the main GUI for NXBrew-dl. It's where the magic happens! """ super().__init__() # Load in main GUI self.ui = Ui_nxbrew_dl() self.ui.setupUi(self) # Set the window icon icon_path = os.path.join(os.path.dirname(__file__), "img", "logo.svg") icon = QIcon() icon.addFile(icon_path, QSize(), QIcon.Mode.Normal, QIcon.State.Off) self.setWindowIcon(icon) # Set up the logger self.logger = NXBrewLogger(log_level="INFO") self.logger.warning("Do not close this window!") # Check for version updates self.logger.info("Checking for new versions online") github_version, github_url = check_github_version() local_version = nxbrew_dl.__version__ new_version_available = False if Version(local_version) < Version(github_version): self.logger.info("New version of NXBrew-dl available!") new_version_available = True else: self.logger.info("You have the latest version of NXBrew-dl") self.update_notification = self.setup_update_notification( new_version_available, url=github_url, ) # Load in various config files self.mod_dir = os.path.dirname(nxbrew_dl.__file__) general_config_filename = os.path.join(self.mod_dir, "configs", "general.yml") self.general_config = load_yml(general_config_filename) regex_config_filename = os.path.join(self.mod_dir, "configs", "regex.yml") self.regex_config = load_yml(regex_config_filename) # Read in the user config, keeping the filename around so we can save it out later self.user_config_file = os.path.join(os.getcwd(), "config.yml") if os.path.exists(self.user_config_file): self.user_config = load_yml(self.user_config_file) else: self.user_config = {} # Load in regions/languages popup self.regions_languages = RegionLanguageWindow( general_config=self.general_config, user_config=self.user_config, ) reg_lang_button = self.ui.pushButtonRegionLanguage reg_lang_button.clicked.connect(lambda: self.regions_languages.show()) # Read in user cache, keeping the filename around so we can save it out later self.user_cache_file = os.path.join(os.getcwd(), "cache.json") if os.path.exists(self.user_cache_file): self.user_cache = load_json(self.user_cache_file) else: self.user_cache = {} # Do an initial load of the config self.load_config() # Set up the worker threads for later self.nxbrew_thread = None self.nxbrew_worker = None # Help menu buttons documentation = self.ui.actionDocumentation documentation.triggered.connect( lambda: open_url("https://nxbrew-dl.readthedocs.io") ) issues = self.ui.actionIssues issues.triggered.connect( lambda: open_url("https://github.com/bbtufty/nxbrew-dl/issues") ) about = self.ui.actionAbout about.triggered.connect(lambda: AboutWindow(self).exec()) # Main window buttons run_nxbrew_dl = self.ui.pushButtonRun run_nxbrew_dl.clicked.connect(self.run_nxbrew_dl) exit_button = self.ui.pushButtonExit exit_button.clicked.connect(self.close) # Directory browsing for the download directory self.ui.pushButtonDownloadDir.clicked.connect( partial(self.set_directory_name, line_edit=self.ui.lineEditDownloadDir) ) self.game_table = self.ui.tableGames self.game_dict = {} # Add in refresh option refresh_button = self.ui.pushButtonRefresh refresh_button.clicked.connect(self.load_table) # Set up the table so links will open the webpages self.game_table.itemDoubleClicked.connect(open_game_url) # Set up the search bar self.search_bar = self.ui.lineEditSearch self.search_bar.textChanged.connect(self.update_display) self.load_table()
[docs] def setup_update_notification( self, new_version_available, url, ): """Create a message box to open up to the latest GitHub release""" if not new_version_available: return None # Open up a dialogue box to go to the webpage update_box = QMessageBox() reply = update_box.question( self, "Version update!", "Open latest GitHub release?", QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, ) if reply == QMessageBox.StandardButton.Yes: self.logger.info("Opening GitHub, and closing down") open_url(url) sys.exit() return update_box
[docs] def get_game_dict(self): """Get game dictionary from NXBrew A-Z page""" if "nxbrew" not in self.user_config.get("nxbrew_url", ""): self.logger.warning( "NXBrew URL not found. Enter one and refresh the game list!" ) return False try: _ = requests.get(self.user_config["nxbrew_url"]) except (requests.exceptions.SSLError, requests.exceptions.MissingSchema) as e: self.logger.warning( "Error found in NXBrew URL! Enter one that works and refresh the game list!" ) return False try: self.game_dict = get_game_dict( general_config=self.general_config, regex_config=self.regex_config, nxbrew_url=self.user_config["nxbrew_url"], ) except Exception as e: self.logger.warning( "Error found retreiving game list, try another URL" ) return False
[docs] def update_display(self, text): """When using the search bar, show/hide rows Args: text (str): Text to filter out rows """ for r in range(self.game_table.rowCount()): r_text = self.game_table.item(r, 0).text() if text.lower() in r_text.lower(): self.game_table.showRow(r) else: self.game_table.hideRow(r)
[docs] def load_table(self): """Load the game table, disable things until we're done""" self.ui.centralwidget.setEnabled(False) # Save and load the config self.save_config() self.load_config() self.game_dict = {} self.get_game_dict() # Clear out the old table and search bar self.search_bar.clear() self.game_table.setRowCount(0) # Add rows to the game dict for name in self.game_dict: row = add_row_to_table(self.game_table, self.game_dict[name]) self.game_dict[name].update( { "row": row, } ) # If in cache, check the row here for cache_item in self.user_cache: found_cache_item = False cache_item_path = urlparse(cache_item).path for r in range(self.game_table.rowCount()): table_item_path = urlparse(self.game_table.item(r, 0).toolTip()).path if table_item_path == cache_item_path: self.game_table.item(r, 1).setCheckState(Qt.CheckState.Checked) found_cache_item = True break if found_cache_item: continue self.ui.centralwidget.setEnabled(True)
[docs] def load_config( self, ): """Apply read in config to the GUI""" text_fields = { "nxbrew_url": self.ui.lineEditNXBrewURL, "download_dir": self.ui.lineEditDownloadDir, "jd_device": self.ui.lineEditJDownloaderDevice, "jd_user": self.ui.lineEditJDownloaderUser, "jd_pass": self.ui.lineEditJDownloaderPass, "discord_url": self.ui.lineEditDiscordURL, } bool_switches = { "download_update": self.ui.checkBoxDownloadUpdates, "download_dlc": self.ui.checkBoxDownloadDLC, "dry_run": self.ui.checkBoxDryRun, } # Set text fields for field in text_fields: if field in self.user_config: text_fields[field].setText(self.user_config[field]) # Set preferred filetypes if "prefer_filetype" in self.user_config: prefer_filetype = self.user_config["prefer_filetype"] if prefer_filetype == "NSP": button = self.ui.radioButtonPreferNSP elif prefer_filetype == "XCI": button = self.ui.radioButtonPreferXCI else: raise ValueError( f"Do not understand preferred filetype {prefer_filetype}" ) button.setChecked(True) # Set the boolean switches for bool_switch in bool_switches: if bool_switch in self.user_config: bool_val = self.user_config[bool_switch] bool_switches[bool_switch].setChecked(bool_val) # And finally, load the region/language list self.regions_languages.load_config()
[docs] def save_config( self, ): """Save config to file""" text_fields = { "nxbrew_url": self.ui.lineEditNXBrewURL.text(), "download_dir": self.ui.lineEditDownloadDir.text(), "jd_device": self.ui.lineEditJDownloaderDevice.text(), "jd_user": self.ui.lineEditJDownloaderUser.text(), "jd_pass": self.ui.lineEditJDownloaderPass.text(), "discord_url": self.ui.lineEditDiscordURL.text(), } bool_switches = { "download_update": self.ui.checkBoxDownloadUpdates, "download_dlc": self.ui.checkBoxDownloadDLC, "dry_run": self.ui.checkBoxDryRun, } for field in text_fields: self.user_config[field] = text_fields[field] # Set the NSP/XCI preferences prefer_filetype = self.ui.buttonGroupPreferNSPXCI.checkedButton().text() if prefer_filetype == "Prefer NSPs": self.user_config["prefer_filetype"] = "NSP" elif prefer_filetype == "Prefer XCIs": self.user_config["prefer_filetype"] = "XCI" else: raise ValueError(f"Button {prefer_filetype} not understood") # Set the boolean switches for bool_switch in bool_switches: self.user_config[bool_switch] = bool_switches[bool_switch].isChecked() # Set region/language priorities (only if there are some!) regions = get_ordered_list( self.regions_languages.ui.listWidgetConfigRegionsLanguagesRegions ) if len(regions) > 0: self.user_config["regions"] = regions languages = get_ordered_list( self.regions_languages.ui.listWidgetConfigRegionsLanguagesLanguages ) if len(languages) > 0: self.user_config["languages"] = languages save_yml(self.user_config_file, self.user_config) return True
[docs] def set_directory_name( self, line_edit, ): """Make a button set a directory name Args: line_edit (QLineEdit): The QLineEdit widget to set the text for """ filename = QFileDialog.getExistingDirectory( self, caption=self.tr("Select directory"), dir=os.getcwd(), ) if filename != "": line_edit.setText(filename)
[docs] @Slot() def run_nxbrew_dl(self): """Run NXBrew-dl""" # Start out by saving the config self.save_config() # Close any other windows self.regions_languages.close() # Get a list of things to download to_download = {} for r in range(self.game_table.rowCount()): if self.game_table.item(r, 1).checkState() == Qt.CheckState.Checked: url = self.game_table.item(r, 0).toolTip() for g in self.game_dict: if self.game_dict[g]["url"] == url: n = self.game_dict[g]["short_name"] to_download.update({n: url}) # Set up everything so the GUI doesn't hang self.nxbrew_thread = QThread() self.nxbrew_worker = NXBrewWorker( to_download=to_download, progress_bar=self.ui.progressBar, progress_bar_label=self.ui.labelProgressBar, user_config=self.user_config, user_cache=self.user_cache, logger=self.logger, ) self.nxbrew_worker.moveToThread(self.nxbrew_thread) self.nxbrew_thread.started.connect(self.nxbrew_worker.run) # Delete the thread once we're done self.nxbrew_worker.finished.connect(self.nxbrew_thread.quit) self.nxbrew_worker.finished.connect(self.nxbrew_worker.deleteLater) self.nxbrew_thread.finished.connect(self.nxbrew_thread.deleteLater) # When finished, re-enable the UI self.nxbrew_thread.finished.connect( lambda: self.enable_disable_ui(mode="enable") ) # Start the thread self.nxbrew_thread.start() # Disable the UI self.enable_disable_ui(mode="disable") return True
[docs] def closeEvent(self, event): """Close the application""" # Check if we've fully loaded, else just close it down loaded = hasattr(self, "user_config") if loaded: self.logger.info("Closing down. Will save config") self.save_config() event.accept()
[docs] def enable_disable_ui(self, mode="disable"): """Selective enable/disable parts of the UI Args: mode: Whether to 'enable' or 'disable'. Defaults to disable """ # Disable the various UI elements ui_elements = [ self.ui.lineEditNXBrewURL, self.ui.lineEditDownloadDir, self.ui.pushButtonDownloadDir, self.ui.lineEditJDownloaderDevice, self.ui.lineEditJDownloaderUser, self.ui.lineEditJDownloaderPass, self.ui.radioButtonPreferNSP, self.ui.radioButtonPreferXCI, self.ui.checkBoxDownloadUpdates, self.ui.checkBoxDownloadDLC, self.ui.pushButtonRegionLanguage, self.ui.checkBoxDryRun, self.ui.lineEditDiscordURL, self.ui.tableGames, self.ui.pushButtonRefresh, self.ui.pushButtonRun, self.ui.pushButtonExit, ] for e in ui_elements: if mode == "disable": e.setEnabled(False) elif mode == "enable": e.setEnabled(True) else: raise ValueError( f"Button {mode} should be one of 'disable' or 'enable'" ) return True
class NXBrewWorker(QObject): """Handles running NXBrew so GUI doesn't hang""" finished = Signal() def __init__( self, to_download, progress_bar=None, progress_bar_label=None, general_config=None, regex_config=None, user_config=None, user_cache=None, logger=None, ): """Initialise the NXBrew downloader Args: to_download (dict): Dictionary of ROMs to download progress_bar (QProgressBar, optional): Progress bar widget. Defaults to None, which will do nothing fancy with the progress bar progress_bar_label (QLabel, optional): If set, will put the game title in a progress bar label. Defaults to None general_config (dict): Dictionary of general configuration. Defaults to None, which will load in from expected path regex_config (dict): Dictionary of regex configuration. Defaults to None, which will load in from expected path user_config (dict): Dictionary of user configuration. Defaults to None, which will load in from expected path user_cache (dict): Dictionary of user cache configuration. Defaults to None, which will load in from expected path logger (logging.Logger): Logger instance. Defaults to None, which will set up its own logger """ super().__init__() self.to_download = to_download self.progress_bar = progress_bar self.progress_bar_label = progress_bar_label self.general_config = general_config self.regex_config = regex_config self.user_config = user_config self.user_cache = user_cache self.logger = logger def run(self): """Run NXBrew-dl""" try: nx = NXBrew( to_download=self.to_download, progress_bar=self.progress_bar, progress_bar_label=self.progress_bar_label, general_config=self.general_config, regex_config=self.regex_config, user_config=self.user_config, user_cache=self.user_cache, logger=self.logger, ) nx.run() except (Exception, MYJDException): tb = traceback.format_exc() for line in tb.splitlines(): self.logger.warning(line) # Sleep a little to avoid potential hangups time.sleep(1) self.finished.emit()