def __init__(self, name="Minigalaxy"): Gtk.ApplicationWindow.__init__(self, title=name) self.api = Api() self.search_string = "" self.offline = False # Set library self.library = Library(self, self.api) self.window_library.add(self.library) self.header_installed.set_active(Config.get("installed_filter")) # Set the icon icon = GdkPixbuf.Pixbuf.new_from_file(LOGO_IMAGE_PATH) self.set_default_icon_list([icon]) # Show the window if Config.get("keep_window_maximized"): self.maximize() self.show_all() # Create the thumbnails directory if not os.path.exists(THUMBNAIL_DIR): os.makedirs(THUMBNAIL_DIR, mode=0o755) # Interact with the API self.__authenticate() self.HeaderBar.set_subtitle(self.api.get_user_info()) self.sync_library()
def __get_installed_games(self) -> List[Game]: games = [] directories = os.listdir(Config.get("install_dir")) for directory in directories: full_path = os.path.join(Config.get("install_dir"), directory) # Only scan directories if not os.path.isdir(full_path): continue # Make sure the gameinfo file exists gameinfo = os.path.join(full_path, "gameinfo") if not os.path.isfile(gameinfo): continue with open(gameinfo, 'r') as file: name = file.readline().strip() version = file.readline().strip() version_dev = file.readline().strip() language = file.readline().strip() game_id = file.readline().strip() if not game_id: game_id = 0 else: game_id = int(game_id) games.append( Game(name=name, game_id=game_id, install_dir=full_path)) return games
def save_pressed(self, button): self.__save_language_choice() Config.set("keep_installers", self.switch_keep_installers.get_active()) Config.set("stay_logged_in", self.switch_stay_logged_in.get_active()) if self.switch_show_windows_games.get_active() != Config.get( "show_windows_games"): if self.switch_show_windows_games.get_active( ) and not shutil.which("wine"): self.parent.show_error( _("Wine wasn't found. Showing Windows games cannot be enabled." )) Config.set("show_windows_games", False) else: Config.set("show_windows_games", self.switch_show_windows_games.get_active()) self.parent.reset_library() # Only change the install_dir is it was actually changed if self.button_file_chooser.get_filename() != Config.get( "install_dir"): if self.__save_install_dir_choice(): DownloadManager.cancel_all_downloads() self.parent.reset_library() else: self.parent.show_error( _("{} isn't a usable path").format( self.button_file_chooser.get_filename())) self.destroy()
def remove_installer(game, installer): error_message = "" installer_directory = os.path.dirname(installer) if not os.path.isdir(installer_directory): error_message = "No installer directory is present: {}".format( installer_directory) return error_message if Config.get("keep_installers"): keep_dir = os.path.join(Config.get("install_dir"), "installer") keep_dir2 = os.path.join(keep_dir, game.get_install_directory_name()) if keep_dir2 == installer_directory: # We are using the keep installer already return error_message if not compare_directories(installer_directory, keep_dir2): shutil.rmtree(keep_dir2, ignore_errors=True) try: shutil.move(installer_directory, keep_dir2) except Exception as e: error_message = str(e) else: os.remove(installer) return error_message
def __authenticate(self): url = None if Config.get("stay_logged_in"): token = Config.get("refresh_token") else: Config.unset("username") Config.unset("refresh_token") token = None # Make sure there is an internet connection if not self.api.can_connect(): return authenticated = self.api.authenticate(refresh_token=token, login_code=url) while not authenticated: login_url = self.api.get_login_url() redirect_url = self.api.get_redirect_url() login = Login(login_url=login_url, redirect_url=redirect_url, parent=self) response = login.run() login.hide() if response == Gtk.ResponseType.DELETE_EVENT: Gtk.main_quit() exit(0) if response == Gtk.ResponseType.NONE: result = login.get_result() authenticated = self.api.authenticate(refresh_token=token, login_code=result) Config.set("refresh_token", authenticated)
def save_pressed(self, button): self.__save_language_choice() Config.set("keep_installers", self.switch_keep_installers.get_active()) Config.set("stay_logged_in", self.switch_stay_logged_in.get_active()) Config.set("show_fps", self.switch_show_fps.get_active()) if self.switch_show_windows_games.get_active() != Config.get( "show_windows_games"): Config.set("show_windows_games", self.switch_show_windows_games.get_active()) self.parent.reset_library() # Only change the install_dir is it was actually changed if self.button_file_chooser.get_filename() != Config.get( "install_dir"): if self.__save_install_dir_choice(): DownloadManager.cancel_all_downloads() self.parent.reset_library() else: dialog = Gtk.MessageDialog( parent=self, modal=True, destroy_with_parent=True, message_type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK, text=_("{} isn't a usable path").format( self.button_file_chooser.get_filename())) dialog.run() dialog.destroy() self.destroy()
def __init__(self, parent): Gtk.Dialog.__init__(self, title=_("Preferences"), parent=parent, modal=True) self.parent = parent self.__set_language_list() self.button_file_chooser.set_filename(Config.get("install_dir")) self.switch_keep_installers.set_active(Config.get("keep_installers")) self.switch_stay_logged_in.set_active(Config.get("stay_logged_in")) self.switch_show_fps.set_active(Config.get("show_fps")) self.switch_show_windows_games.set_active( Config.get("show_windows_games")) # Set tooltip for keep installers label installer_dir = os.path.join(self.button_file_chooser.get_filename(), "installer") self.label_keep_installers.set_tooltip_text( _("Keep installers after downloading a game.\nInstallers are stored in: {}" ).format(installer_dir)) # Only allow showing Windows games if wine is available if not shutil.which("wine"): self.switch_show_windows_games.set_sensitive(False) self.switch_show_windows_games.set_tooltip_text( _("Install Wine to enable this feature"))
def __get_installed_games(self) -> List[Game]: # Make sure the install directory exists library_dir = Config.get("install_dir") if not os.path.exists(library_dir): os.makedirs(library_dir, mode=0o755) directories = os.listdir(library_dir) games = [] for directory in directories: full_path = os.path.join(Config.get("install_dir"), directory) # Only scan directories if not os.path.isdir(full_path): continue # Make sure the gameinfo file exists gameinfo = os.path.join(full_path, "gameinfo") if os.path.isfile(gameinfo): with open(gameinfo, 'r') as file: name = file.readline().strip() version = file.readline().strip() # noqa: F841 version_dev = file.readline().strip() # noqa: F841 language = file.readline().strip() # noqa: F841 game_id = file.readline().strip() if not game_id: game_id = 0 else: game_id = int(game_id) games.append( Game(name=name, game_id=game_id, install_dir=full_path)) else: games.extend(get_installed_windows_games(full_path)) return games
def __set_fps_display(): # Enable FPS Counter for Nvidia or AMD (Mesa) users if Config.get("show_fps"): os.environ["__GL_SHOW_GRAPHICS_OSD"] = "1" # For Nvidia users os.environ["GALLIUM_HUD"] = "simple,fps" # For AMDGPU users elif Config.get("show_fps") is False: os.environ["__GL_SHOW_GRAPHICS_OSD"] = "0" # For Nvidia users os.environ["GALLIUM_HUD"] = ""
def __set_fps_display(): # Enable FPS Counter for Nvidia or AMD (Mesa) users if Config.get("show_fps"): os.environ["__GL_SHOW_GRAPHICS_OSD"] = "1" # For Nvidia users + OpenGL/Vulkan games os.environ["GALLIUM_HUD"] = "simple,fps" # For AMDGPU users + OpenGL games os.environ["VK_INSTANCE_LAYERS"] = "VK_LAYER_MESA_overlay" # For AMDGPU users + Vulkan games elif Config.get("show_fps") is False: os.environ["__GL_SHOW_GRAPHICS_OSD"] = "0" os.environ["GALLIUM_HUD"] = "" os.environ["VK_INSTANCE_LAYERS"] = ""
def install_game(game, installer, parent_window=None) -> None: if not os.path.exists(installer): GLib.idle_add(__show_installation_error, game, _("{} failed to download.").format(installer), parent_window) raise FileNotFoundError("The installer {} does not exist".format(installer)) if not __verify_installer_integrity(installer): GLib.idle_add(__show_installation_error, game, _("{} was corrupted. Please download it again.").format(installer), parent_window) os.remove(installer) raise FileNotFoundError("The installer {} was corrupted".format(installer)) # Make a temporary empty directory for extracting the installer temp_dir = os.path.join(CACHE_DIR, "extract/{}".format(game.id)) if os.path.exists(temp_dir): shutil.rmtree(temp_dir, ignore_errors=True) os.makedirs(temp_dir) # Extract the installer try: with zipfile.ZipFile(installer) as archive: for member in archive.namelist(): file = archive.getinfo(member) archive.extract(file, temp_dir) # Set permissions attr = file.external_attr >> 16 os.chmod(os.path.join(temp_dir, member), attr) except zipfile.BadZipFile as e: GLib.idle_add(__show_installation_error, game, _("{} could not be unzipped.").format(installer), parent_window) raise e # Make sure the install directory exists library_dir = Config.get("install_dir") if not os.path.exists(library_dir): os.makedirs(library_dir) # Copy the game files into the correct directory shutil.move(os.path.join(temp_dir, "data/noarch"), game.install_dir) shutil.copyfile( os.path.join(THUMBNAIL_DIR, "{}.jpg".format(game.id)), os.path.join(game.install_dir, "thumbnail.jpg"), ) # Remove the temporary directory shutil.rmtree(temp_dir, ignore_errors=True) if Config.get("keep_installers"): keep_dir = os.path.join(Config.get("install_dir"), "installer") if not os.path.exists(keep_dir): os.makedirs(keep_dir) try: os.rename(installer, os.path.join(keep_dir, "{}.sh".format(game.name))) except Exception as ex: print("Encountered error while copying {} to {}. Got error: {}".format(installer, keep_dir, ex)) else: os.remove(installer)
def __init__(self, name="Minigalaxy"): current_locale = Config.get("locale") default_locale = locale.getdefaultlocale()[0] if current_locale == '': locale.setlocale(locale.LC_ALL, (default_locale, 'UTF-8')) else: try: locale.setlocale(locale.LC_ALL, (current_locale, 'UTF-8')) except NameError: locale.setlocale(locale.LC_ALL, (default_locale, 'UTF-8')) Gtk.ApplicationWindow.__init__(self, title=name) self.api = Api() self.search_string = "" self.offline = False # Set library self.library = Library(self, self.api) self.window_library.add(self.library) self.header_installed.set_active(Config.get("installed_filter")) # Set the icon icon = GdkPixbuf.Pixbuf.new_from_file(LOGO_IMAGE_PATH) self.set_default_icon_list([icon]) # Set theme settings = Gtk.Settings.get_default() if Config.get("use_dark_theme") is True: settings.set_property("gtk-application-prefer-dark-theme", True) else: settings.set_property("gtk-application-prefer-dark-theme", False) # Show the window if Config.get("keep_window_maximized"): self.maximize() self.show_all() self.make_directories() # Interact with the API self.offline = not self.api.can_connect() if not self.offline: try: self.__authenticate() self.HeaderBar.set_subtitle(self.api.get_user_info()) except Exception as e: print( "Starting in offline mode, after receiving exception: {}". format(e)) self.offline = True self.sync_library()
def __init__(self, parent, game): current_locale = Config.get("locale") default_locale = locale.getdefaultlocale()[0] if current_locale == '': locale.setlocale(locale.LC_ALL, (default_locale, 'UTF-8')) else: try: locale.setlocale(locale.LC_ALL, (current_locale, 'UTF-8')) except NameError: locale.setlocale(locale.LC_ALL, (default_locale, 'UTF-8')) Gtk.Frame.__init__(self) Gtk.StyleContext.add_provider(self.button.get_style_context(), CSS_PROVIDER, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) self.parent = parent self.game = game self.api = parent.api self.offline = parent.offline self.progress_bar = None self.thumbnail_set = False self.download_list = [] self.dlc_dict = {} self.current_state = self.state.DOWNLOADABLE self.image.set_tooltip_text(self.game.name) self.game_label.set_label(self.game.name) # Set folder for download installer self.download_dir = os.path.join( CACHE_DIR, "download", self.game.get_install_directory_name()) # Set folder if user wants to keep installer (disabled by default) self.keep_dir = os.path.join(Config.get("install_dir"), "installer") self.keep_path = os.path.join(self.keep_dir, self.game.get_install_directory_name()) if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR, mode=0o755) self.reload_state() load_thumbnail_thread = threading.Thread(target=self.load_thumbnail) load_thumbnail_thread.start() # Start download if Minigalaxy was closed while downloading this game self.resume_download_if_expected() # Icon for Windows games if self.game.platform == "windows": self.image.set_tooltip_text("{} (Wine)".format(self.game.name)) self.wine_icon.set_from_file(ICON_WINE_PATH) self.wine_icon.show()
def remove_installer(game, installer): error_message = "" if Config.get("keep_installers"): keep_dir = os.path.join(Config.get("install_dir"), "installer") keep_dir2 = os.path.join(keep_dir, game.get_install_directory_name()) keep_file = os.path.join(keep_dir2, os.path.basename(installer)) if not os.path.isdir(keep_dir2): os.makedirs(keep_dir2) shutil.move(installer, keep_file) installer_directory = os.path.dirname(installer) if os.path.isdir(installer_directory): shutil.rmtree(installer_directory, ignore_errors=True) else: error_message = "No installer directory is present: {}".format(installer_directory) return error_message
def __init__(self, parent): Gtk.Dialog.__init__(self, title=_("Preferences"), parent=parent, modal=True) self.parent = parent self.__set_language_list() self.button_file_chooser.set_filename(Config.get("install_dir")) self.switch_keep_installers.set_active(Config.get("keep_installers")) self.switch_stay_logged_in.set_active(Config.get("stay_logged_in")) self.switch_show_fps.set_active(Config.get("show_fps")) self.switch_show_windows_games.set_active(Config.get("show_windows_games")) # Set tooltip for keep installers label installer_dir = os.path.join(self.button_file_chooser.get_filename(), "installer") self.label_keep_installers.set_tooltip_text( _("Keep installers after downloading a game.\nInstallers are stored in: {}").format(installer_dir) )
def get_download_info(self, game: Game, operating_system="linux", dlc_installers="") -> dict: if dlc_installers: installers = dlc_installers else: response = self.get_info(game) installers = response["downloads"]["installers"] possible_downloads = [] for installer in installers: if installer["os"] == operating_system: possible_downloads.append(installer) if not possible_downloads: if operating_system == "linux": return self.get_download_info(game, "windows") else: raise NoDownloadLinkFound( "Error: {} with id {} couldn't be installed".format( game.name, game.id)) download_info = possible_downloads[0] for installer in possible_downloads: if installer['language'] == Config.get("lang"): download_info = installer break if installer['language'] == "en": download_info = installer # Return last entry in possible_downloads. This will either be English or the first langauge in the list # This is just a backup, if the preferred language has been found, this part won't execute return download_info
def __save_install_dir_choice(self) -> bool: choice = self.button_file_chooser.get_filename() old_dir = Config.get("install_dir") if choice == old_dir: return True if not os.path.exists(choice): try: os.makedirs(choice, mode=0o755) except: return False else: write_test_file = os.path.join(choice, "write_test.txt") try: with open(write_test_file, "w") as file: file.write("test") file.close() os.remove(write_test_file) except: return False # Remove the old directory if it is empty try: os.rmdir(old_dir) except OSError: pass Config.set("install_dir", choice) return True
def test_create_config(self, mock_exists, mock_isdir): mock_exists.side_effect = [False, True] mock_isdir.return_value = True with patch("builtins.open", mock_open()) as mock_config: from minigalaxy.config import Config Config.first_run = False Config.get("") mock_c = mock_config.mock_calls write_string = "" for kall in mock_c: name, args, kwargs = kall if name == "().write": write_string = "{}{}".format(write_string, args[0]) exp = JSON_DEFAULT_CONFIGURATION obs = write_string self.assertEqual(exp, obs)
def __init__(self, parent, game, api): Gtk.Frame.__init__(self) self.parent = parent self.game = game self.api = api self.progress_bar = None self.thumbnail_set = False self.download = None self.current_state = self.state.DOWNLOADABLE self.image.set_tooltip_text(self.game.name) # Set folder for download installer self.download_dir = os.path.join(CACHE_DIR, "download") self.download_path = os.path.join(self.download_dir, "{}.sh".format(self.game.name)) # Set folder if user wants to keep installer (disabled by default) self.keep_dir = os.path.join(Config.get("install_dir"), "installer") self.keep_path = os.path.join(self.keep_dir, "{}.sh".format(self.game.name)) if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR) self.reload_state() self.load_thumbnail() # Start download if Minigalaxy was closed while downloading this game self.resume_download_if_expected()
def __download(self, download_info, finish_func, cancel_to_state): download_success = True GLib.idle_add(self.update_to_state, self.state.QUEUED) Config.set("current_download", self.game.id) # Start the download for all files self.download_list = [] number_of_files = len(download_info['files']) total_file_size = 0 executable_path = None download_files = [] for key, file_info in enumerate(download_info['files']): try: download_url = self.api.get_real_download_link( file_info["downlink"]) except ValueError as e: print(e) GLib.idle_add(self.parent.parent.show_error, _("Download error"), _(str(e))) download_success = False break total_file_size += self.api.get_file_size(file_info["downlink"]) try: # Extract the filename from the download url (filename is between %2F and &token) filename = urllib.parse.unquote( re.search('%2F(((?!%2F).)*)&t', download_url).group(1)) except AttributeError: filename = "{}-{}.bin".format(self.game.get_stripped_name(), key) download_path = os.path.join(self.download_dir, filename) if key == 0: # If key = 0, denote the file as the executable's path executable_path = download_path md5sum = self.api.get_download_file_md5(file_info["downlink"]) if md5sum: self.game.md5sum[os.path.basename(download_path)] = md5sum download = Download( url=download_url, save_location=download_path, finish_func=finish_func if download_path == executable_path else None, progress_func=self.set_progress, cancel_func=lambda: self.__cancel(to_state=cancel_to_state), number=number_of_files - key, out_of_amount=number_of_files, game=self.game) download_files.insert(0, download) self.download_list.extend(download_files) if check_diskspace(total_file_size, Config.get("install_dir")): DownloadManager.download(download_files) ds_msg_title = "" ds_msg_text = "" else: ds_msg_title = "Download error" ds_msg_text = "Not enough disk space to install game." download_success = False if ds_msg_title: GLib.idle_add(self.parent.parent.show_error, _(ds_msg_title), _(ds_msg_text)) return download_success
def create_applications_file(game): error_message = "" if Config.get("create_applications_file"): path_to_shortcut = os.path.join(APPLICATIONS_DIR, "{}.desktop".format(game.name)) exe_cmd = get_exec_line(game) # Create desktop file definition desktop_context = { "game_bin_path": exe_cmd, "game_name": game.name, "game_install_dir": game.install_dir, "game_icon_path": os.path.join(game.install_dir, 'support/icon.png') } desktop_definition = """\ [Desktop Entry] Type=Application Terminal=false StartupNotify=true Exec={game_bin_path} Path={game_install_dir} Name={game_name} Icon={game_icon_path}""".format(**desktop_context) if not os.path.isfile(path_to_shortcut): try: with open(path_to_shortcut, 'w+') as desktop_file: desktop_file.writelines( textwrap.dedent(desktop_definition)) except Exception as e: os.remove(path_to_shortcut) error_message = e return error_message
def load_description(self): description = "" lang = Config.get("lang") if self.gamesdb_info["summary"]: desc_lang = "*" for summary_key in self.gamesdb_info["summary"].keys(): if lang in summary_key: desc_lang = summary_key description_len = 470 if len(self.gamesdb_info["summary"][desc_lang]) > description_len: description = "{}...".format( self.gamesdb_info["summary"][desc_lang][:description_len]) else: description = self.gamesdb_info["summary"][desc_lang] if "*" in self.gamesdb_info["genre"]: genre = self.gamesdb_info["genre"]["*"] else: genre = _("unknown") for genre_key, genre_value in self.gamesdb_info["genre"].items(): if lang in genre_key: genre = genre_value description = "{}: {}\n{}".format(_("Genre"), genre, description) if self.game.is_installed(): description = "{}: {}\n{}".format(_("Version"), self.game.get_info("version"), description) GLib.idle_add(self.label_game_description.set_text, description)
def __save_theme_choice(self) -> None: settings = Gtk.Settings.get_default() Config.set("use_dark_theme", self.switch_use_dark_theme.get_active()) if Config.get("use_dark_theme") is True: settings.set_property("gtk-application-prefer-dark-theme", True) else: settings.set_property("gtk-application-prefer-dark-theme", False)
def __set_install_dir(self): if not self.game.install_dir: self.game.install_dir = os.path.join( Config.get("install_dir"), self.game.get_install_directory_name()) self.game.status_file_path = os.path.join( self.game.install_dir, self.game.status_file_name)
def __request(self, url: str = None, params: dict = None) -> dict: # Refresh the token if needed if self.active_token_expiration_time < time.time(): print("Refreshing token") refresh_token = Config.get("refresh_token") Config.set("refresh_token", self.__refresh_token(refresh_token)) # Make the request headers = { 'Authorization': "Bearer {}".format(str(self.active_token)), } result = {} try: response = SESSION.get(url, headers=headers, params=params) if self.debug: print("Request: {}".format(url)) print("Return code: {}".format(response.status_code)) print("Response body: {}".format(response.text)) print("") if response.status_code < 300: result = response.json() except requests.exceptions.RequestException as e: print("Encountered exception while making HTTP request.") print("Request: {}".format(url)) print("Exception: {}".format(e)) print("") return result
def get_library(self): if not self.active_token: return games = [] current_page = 1 all_pages_processed = False url = "https://embed.gog.com/account/getFilteredProducts" while not all_pages_processed: params = { 'mediaType': 1, # 1 means game 'page': current_page, } response = self.__request(url, params=params) total_pages = response["totalPages"] for product in response["products"]: if product["id"] not in IGNORE_GAME_IDS: # Only support Linux unless the show_windows_games setting is enabled if product["worksOn"]["Linux"]: platform = "linux" elif Config.get("show_windows_games"): platform = "windows" else: continue if not product["url"]: print("{} ({}) has no store page url".format(product["title"], product['id'])) game = Game(name=product["title"], url=product["url"], game_id=product["id"], image_url=product["image"], platform=platform) games.append(game) if current_page == total_pages: all_pages_processed = True current_page += 1 return games
def get_user_info(self) -> str: username = Config.get("username") if not username: url = "https://embed.gog.com/userData.json" response = self.__request(url) username = response["username"] Config.set("username", username) return username
def __save_view_choice(self) -> None: view_choice = self.combobox_view.get_active_iter() if view_choice is not None: model = self.combobox_view.get_model() view, _ = model[view_choice][:2] if view != Config.get("view"): self.parent.reset_library() Config.set("view", view)
def remove_installer(installer): error_message = "" if Config.get("keep_installers"): keep_dir = os.path.join(Config.get("install_dir"), "installer") download_dir = os.path.join(CACHE_DIR, "download") if not os.path.exists(keep_dir): os.makedirs(keep_dir, mode=0o755) try: # It's needed for multiple files for file in os.listdir(download_dir): shutil.move(download_dir + '/' + file, keep_dir + '/' + file) except Exception as ex: print("Encountered error while copying {} to {}. Got error: {}". format(installer, keep_dir, ex)) elif os.path.exists(installer): os.remove(installer) return error_message
def __add_gametile(self, game): view = Config.get("view") if view == "grid": self.flowbox.add(GameTile(self, game)) elif view == "list": self.flowbox.add(GameTileList(self, game)) self.sort_library() self.flowbox.show_all()