mirror of
https://github.com/NinjaCheetah/NUSGet.git
synced 2026-02-28 07:35:30 -05:00
Merge remote-tracking branch 'upstream/main'
# Conflicts: # modules/download_wii.py # qt/py/ui_AboutDialog.py
This commit is contained in:
@@ -54,6 +54,7 @@ def connect_label_to_checkbox(label, checkbox):
|
||||
checkbox.toggle()
|
||||
label.mousePressEvent = toggle_checkbox
|
||||
|
||||
|
||||
def connect_is_enabled_to_checkbox(items, chkbox):
|
||||
for item in items:
|
||||
if chkbox.isChecked():
|
||||
@@ -61,6 +62,7 @@ def connect_is_enabled_to_checkbox(items, chkbox):
|
||||
else:
|
||||
item.setEnabled(False)
|
||||
|
||||
|
||||
def check_nusget_updates(app, current_version: str, progress_callback=None) -> str | None:
|
||||
# Simple function to make a request to the GitHub API and then check if the latest available version is newer.
|
||||
gh_api_request = requests.get(url="https://api.github.com/repos/NinjaCheetah/NUSGet/releases/latest", stream=True)
|
||||
@@ -80,6 +82,7 @@ def check_nusget_updates(app, current_version: str, progress_callback=None) -> s
|
||||
progress_callback.emit(app.translate("MainWindow", "\n\nYou're running the latest release of NUSGet."))
|
||||
return None
|
||||
|
||||
|
||||
def get_config_file() -> pathlib.Path:
|
||||
config_dir = pathlib.Path(os.path.join(
|
||||
os.environ.get('APPDATA') or
|
||||
@@ -90,11 +93,13 @@ def get_config_file() -> pathlib.Path:
|
||||
config_dir.mkdir(exist_ok=True)
|
||||
return config_dir.joinpath("config.json")
|
||||
|
||||
|
||||
def save_config(config_data: dict) -> None:
|
||||
config_file = get_config_file()
|
||||
print(f"writing data: {config_data}")
|
||||
open(config_file, "w").write(json.dumps(config_data))
|
||||
|
||||
|
||||
def update_setting(config_data: dict, setting: str, value: any) -> None:
|
||||
config_data[setting] = value
|
||||
save_config(config_data)
|
||||
|
||||
@@ -53,5 +53,5 @@ def run_nus_download_batch(out_folder: pathlib.Path, titles: List[BatchTitleData
|
||||
# failed title.
|
||||
result = 1
|
||||
failed_titles.append(title.tid)
|
||||
progress_callback.emit(f"Batch download finished.")
|
||||
progress_callback.emit(0, 1, f"Batch download finished.")
|
||||
return BatchResults(result, warning_titles, failed_titles)
|
||||
|
||||
@@ -29,10 +29,10 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
title_dir.mkdir(exist_ok=True)
|
||||
# Announce the title being downloaded, and the version if applicable.
|
||||
if title_version is not None:
|
||||
progress_callback.emit(f"Downloading title {tid} v{title_version}, please wait...")
|
||||
progress_callback.emit(0, 0, f"Downloading title {tid} v{title_version}, please wait...")
|
||||
else:
|
||||
progress_callback.emit(f"Downloading title {tid} vLatest, please wait...")
|
||||
progress_callback.emit(" - Downloading and parsing TMD...")
|
||||
progress_callback.emit(0, 0, f"Downloading title {tid} vLatest, please wait...")
|
||||
progress_callback.emit(-1, -1, " - Downloading and parsing TMD...")
|
||||
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
|
||||
try:
|
||||
if title_version is not None:
|
||||
@@ -50,17 +50,17 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
version_dir.joinpath(f"tmd.{title_version}").write_bytes(title.tmd.dump())
|
||||
# Use a local ticket, if one exists and "use local files" is enabled.
|
||||
if use_local_chkbox and version_dir.joinpath("tik").exists():
|
||||
progress_callback.emit(" - Parsing local copy of Ticket...")
|
||||
progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...")
|
||||
title.load_ticket(version_dir.joinpath("tik").read_bytes())
|
||||
else:
|
||||
progress_callback.emit(" - Downloading and parsing Ticket...")
|
||||
progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...")
|
||||
try:
|
||||
title.load_ticket(libTWLPy.download_ticket(tid))
|
||||
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
||||
except ValueError:
|
||||
# If libTWLPy returns an error, then no ticket is available. Log this, and disable options requiring a
|
||||
# ticket so that they aren't attempted later.
|
||||
progress_callback.emit(" - No Ticket is available!")
|
||||
progress_callback.emit(-1, -1, " - No Ticket is available!")
|
||||
pack_tad_enabled = False
|
||||
decrypt_contents_enabled = False
|
||||
# Load the content record from the TMD, and download the content it lists. DSi titles only have one content.
|
||||
@@ -68,13 +68,13 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
content_file_name = f"{title.tmd.content_record.content_id:08X}"
|
||||
# Check for a local copy of the current content if "use local files" is enabled, and use it.
|
||||
if use_local_chkbox and version_dir.joinpath(content_file_name).exists():
|
||||
progress_callback.emit(" - Using local copy of content")
|
||||
progress_callback.emit(-1, -1, " - Using local copy of content")
|
||||
content = version_dir.joinpath(content_file_name).read_bytes()
|
||||
else:
|
||||
progress_callback.emit(f" - Downloading content (Content ID: {title.tmd.content_record.content_id}, Size: "
|
||||
progress_callback.emit(-1, -1, f" - Downloading content (Content ID: {title.tmd.content_record.content_id}, Size: "
|
||||
f"{title.tmd.content_record.content_size} bytes)...")
|
||||
content = libTWLPy.download_content(tid, title.tmd.content_record.content_id)
|
||||
progress_callback.emit(" - Done!")
|
||||
progress_callback.emit(-1, -1, " - Done!")
|
||||
# If keep encrypted contents is on, write out the content after its downloaded.
|
||||
if keep_enc_chkbox is True:
|
||||
version_dir.joinpath(content_file_name).write_bytes(content)
|
||||
@@ -82,7 +82,7 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
# If decrypt local contents is still true, decrypt the content and write out the decrypted file.
|
||||
if decrypt_contents_enabled is True:
|
||||
try:
|
||||
progress_callback.emit(f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...")
|
||||
progress_callback.emit(-1, -1, f" - Decrypting content (Content ID: {title.tmd.content_record.content_id})...")
|
||||
dec_content = title.get_content()
|
||||
content_file_name = f"{title.tmd.content_record.content_id:08X}.app"
|
||||
version_dir.joinpath(content_file_name).write_bytes(dec_content)
|
||||
@@ -93,10 +93,10 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
# If pack TAD is still true, pack the TMD, ticket, and content into a TAD.
|
||||
if pack_tad_enabled is True:
|
||||
# Get the TAD certificate chain, courtesy of libTWLPy.
|
||||
progress_callback.emit(" - Building certificate...")
|
||||
progress_callback.emit(-1, -1, " - Building certificate...")
|
||||
title.tad.set_cert_data(libTWLPy.download_cert())
|
||||
# Use a typed TAD name if there is one, and auto generate one based on the TID and version if there isn't.
|
||||
progress_callback.emit("Packing TAD...")
|
||||
progress_callback.emit(-1, -1, "Packing TAD...")
|
||||
if tad_file_name != "" and tad_file_name is not None:
|
||||
# Batch downloads may insert -vLatest, so if it did we can fill in the real number now.
|
||||
tad_file_name = tad_file_name.replace("-vLatest", f"-v{title_version}")
|
||||
@@ -104,9 +104,13 @@ def run_nus_download_dsi(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
tad_file_name += ".tad"
|
||||
else:
|
||||
tad_file_name = f"{tid}-v{title_version}.tad"
|
||||
# Certain special characters are prone to breaking things, so strip them from the file name before actually
|
||||
# opening the file for writing. On some platforms (like macOS), invalid characters get replaced automatically,
|
||||
# but on Windows the file will just fail to be written out at all.
|
||||
tad_file_name = tad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'})
|
||||
# Have libTWLPy dump the TAD, and write that data out.
|
||||
version_dir.joinpath(tad_file_name).write_bytes(title.dump_tad())
|
||||
progress_callback.emit("Download complete!")
|
||||
progress_callback.emit(0, 1, "Download complete!")
|
||||
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
|
||||
# point, it means that they enabled decryption or TAD packing for a title that doesn't have a ticket. Return
|
||||
# code 1 so that a warning popup is shown informing them of this.
|
||||
|
||||
@@ -11,6 +11,8 @@ from libWiiPy.title.ticket import _TitleLimit
|
||||
def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_wad_chkbox: bool, keep_enc_chkbox: bool,
|
||||
decrypt_contents_chkbox: bool, wiiu_nus_chkbox: bool, use_local_chkbox: bool,
|
||||
repack_vwii_chkbox: bool, patch_ios: bool, wad_file_name: str, progress_callback=None):
|
||||
def progress_update(done, total):
|
||||
progress_callback.emit(done, total, None)
|
||||
# Actual NUS download function that runs in a separate thread.
|
||||
# Immediately knock out any invalidly formatted Title IDs.
|
||||
if len(tid) != 16:
|
||||
@@ -33,16 +35,16 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
title_dir.mkdir(exist_ok=True)
|
||||
# Announce the title being downloaded, and the version if applicable.
|
||||
if title_version is not None:
|
||||
progress_callback.emit(f"Downloading title {tid} v{title_version}, please wait...")
|
||||
progress_callback.emit(0, 0, f"Downloading title {tid} v{title_version}, please wait...")
|
||||
else:
|
||||
progress_callback.emit(f"Downloading title {tid} vLatest, please wait...")
|
||||
progress_callback.emit(" - Downloading and parsing TMD...")
|
||||
progress_callback.emit(-1, -1, f"Downloading title {tid} vLatest, please wait...")
|
||||
progress_callback.emit(-1, -1, " - Downloading and parsing TMD...")
|
||||
# Download a specific TMD version if a version was specified, otherwise just download the latest TMD.
|
||||
try:
|
||||
if title_version is not None:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled))
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, title_version, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||
else:
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled))
|
||||
title.load_tmd(libWiiPy.title.download_tmd(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||
title_version = title.tmd.title_version
|
||||
# If libWiiPy returns an error, that means that either the TID or version doesn't exist, so return code -2.
|
||||
except ValueError:
|
||||
@@ -55,17 +57,17 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
# Use a local ticket, if one exists and "use local files" is enabled.
|
||||
forge_ticket = False
|
||||
if use_local_chkbox and version_dir.joinpath("tik").exists():
|
||||
progress_callback.emit(" - Parsing local copy of Ticket...")
|
||||
progress_callback.emit(-1, -1, " - Parsing local copy of Ticket...")
|
||||
title.load_ticket(version_dir.joinpath("tik").read_bytes())
|
||||
else:
|
||||
progress_callback.emit(" - Downloading and parsing Ticket...")
|
||||
progress_callback.emit(-1, -1, " - Downloading and parsing Ticket...")
|
||||
try:
|
||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled))
|
||||
title.load_ticket(libWiiPy.title.download_ticket(tid, wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||
version_dir.joinpath("tik").write_bytes(title.ticket.dump())
|
||||
except ValueError:
|
||||
# If libWiiPy returns an error, then no ticket is available. Try to forge a ticket after we download the
|
||||
# content.
|
||||
progress_callback.emit(" - No Ticket is available! Will try forging a Ticket.")
|
||||
progress_callback.emit(0, 0, " - No Ticket is available! Will try forging a Ticket.")
|
||||
forge_ticket = True
|
||||
# Load the content records from the TMD, and begin iterating over the records.
|
||||
title.load_content_records()
|
||||
@@ -75,15 +77,15 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
content_file_name = f"{title.tmd.content_records[content].content_id:08X}"
|
||||
# Check for a local copy of the current content if "use local files" is enabled, and use it.
|
||||
if use_local_chkbox is True and version_dir.joinpath(content_file_name).exists():
|
||||
progress_callback.emit(f" - Using local copy of content {content + 1} of {len(title.tmd.content_records)}")
|
||||
progress_callback.emit(-1, -1, f" - Using local copy of content {content + 1} of {len(title.tmd.content_records)}")
|
||||
content_list.append(version_dir.joinpath(content_file_name).read_bytes())
|
||||
else:
|
||||
progress_callback.emit(f" - Downloading content {content + 1} of {len(title.tmd.content_records)} "
|
||||
progress_callback.emit(0, 0, f" - Downloading content {content + 1} of {len(title.tmd.content_records)} "
|
||||
f"(Content ID: {title.tmd.content_records[content].content_id}, Size: "
|
||||
f"{title.tmd.content_records[content].content_size} bytes)...")
|
||||
content_list.append(libWiiPy.title.download_content(tid, title.tmd.content_records[content].content_id,
|
||||
wiiu_endpoint=wiiu_nus_enabled))
|
||||
progress_callback.emit(" - Done!")
|
||||
wiiu_endpoint=wiiu_nus_enabled, progress=progress_update))
|
||||
progress_callback.emit(-1, -1, " - Done!")
|
||||
# If keep encrypted contents is on, write out each content after its downloaded.
|
||||
if keep_enc_chkbox is True:
|
||||
version_dir.joinpath(content_file_name).write_bytes(content_list[content])
|
||||
@@ -125,7 +127,7 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
if decrypt_contents_enabled is True:
|
||||
try:
|
||||
for content in range(len(title.tmd.content_records)):
|
||||
progress_callback.emit(f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
||||
progress_callback.emit(-1, -1, f" - Decrypting content {content + 1} of {len(title.tmd.content_records)} "
|
||||
f"(Content ID: {title.tmd.content_records[content].content_id})...")
|
||||
dec_content = title.get_content_by_index(content)
|
||||
content_file_name = f"{title.tmd.content_records[content].content_id:08X}.app"
|
||||
@@ -140,15 +142,15 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
# re-encrypted with the common key instead of the vWii key, so that the title can be installed from within
|
||||
# vWii mode. (vWii mode does not have access to the vWii key, only Wii U mode has that.)
|
||||
if repack_vwii_chkbox is True and (tid[3] == "7" or tid[7] == "7"):
|
||||
progress_callback.emit(" - Re-encrypting Title Key with the common key...")
|
||||
progress_callback.emit(-1, -1, " - Re-encrypting Title Key with the common key...")
|
||||
title_key_common = libWiiPy.title.encrypt_title_key(title.ticket.get_title_key(), 0, title.tmd.title_id)
|
||||
title.ticket.common_key_index = 0
|
||||
title.ticket.title_key_enc = title_key_common
|
||||
# Get the WAD certificate chain, courtesy of libWiiPy.
|
||||
progress_callback.emit(" - Building certificate...")
|
||||
progress_callback.emit(-1, -1, " - Building certificate...")
|
||||
title.load_cert_chain(libWiiPy.title.download_cert_chain(wiiu_endpoint=wiiu_nus_enabled))
|
||||
# Use a typed WAD name if there is one, and auto generate one based on the TID and version if there isn't.
|
||||
progress_callback.emit(" - Packing WAD...")
|
||||
progress_callback.emit(-1, -1, " - Packing WAD...")
|
||||
if wad_file_name != "" and wad_file_name is not None:
|
||||
# Batch downloads may insert -vLatest, so if it did we can fill in the real number now.
|
||||
wad_file_name = wad_file_name.replace("-vLatest", f"-v{title_version}")
|
||||
@@ -158,20 +160,24 @@ def run_nus_download_wii(out_folder: pathlib.Path, tid: str, version: str, pack_
|
||||
wad_file_name = f"{tid}-v{title_version}.wad"
|
||||
# If enabled (after we make sure it's an IOS), apply all main IOS patches.
|
||||
if patch_ios and (tid[:8] == "00000001" and int(tid[-2:], 16) > 2):
|
||||
progress_callback.emit(" - Patching IOS...")
|
||||
progress_callback.emit(-1, -1, " - Patching IOS...")
|
||||
ios_patcher = libWiiPy.title.IOSPatcher()
|
||||
ios_patcher.load(title)
|
||||
patch_count = ios_patcher.patch_all()
|
||||
if patch_count > 0:
|
||||
progress_callback.emit(f" - Applied {patch_count} patches!")
|
||||
progress_callback.emit(-1, -1, f" - Applied {patch_count} patches!")
|
||||
else:
|
||||
progress_callback.emit(" - No patches could be applied! Is this a stub IOS?")
|
||||
progress_callback.emit(-1, -1, " - No patches could be applied! Is this a stub IOS?")
|
||||
title = ios_patcher.dump()
|
||||
# Append "-PATCHED" to the end of the WAD file name to make it clear that it was modified.
|
||||
wad_file_name = wad_file_name[:-4] + "-PATCHED" + wad_file_name[-4:]
|
||||
# Certain special characters are prone to breaking things, so strip them from the file name before actually
|
||||
# opening the file for writing. On some platforms (like macOS), invalid characters get replaced automatically,
|
||||
# but on Windows the file will just fail to be written out at all.
|
||||
wad_file_name = wad_file_name.translate({ord(c): None for c in '/\\:*"?<>|'})
|
||||
# Have libWiiPy dump the WAD, and write that data out.
|
||||
version_dir.joinpath(wad_file_name).write_bytes(title.dump_wad())
|
||||
progress_callback.emit("Download complete!")
|
||||
progress_callback.emit(0, 1, "Download complete!")
|
||||
# This is where the variables come in. If the state of these variables doesn't match the user's choice by this
|
||||
# point, it means that they enabled decryption or WAD packing for a title that doesn't have a ticket. Return
|
||||
# code 1 so that a warning popup is shown informing them of this.
|
||||
|
||||
64
modules/theme.py
Normal file
64
modules/theme.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# "modules/theme.py", licensed under the MIT license
|
||||
# Copyright 2024-2025 NinjaCheetah & Contributors
|
||||
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
|
||||
def is_dark_theme_windows():
|
||||
# This has to be here so that Python doesn't try to import it on non-Windows.
|
||||
import winreg
|
||||
try:
|
||||
registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER)
|
||||
key = winreg.OpenKey(registry, r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize")
|
||||
# This value is "AppsUseLightTheme" so a "1" is light and a "0" is dark. Side note: I hate the Windows registry.
|
||||
value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme")
|
||||
return value == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_dark_theme_macos():
|
||||
# macOS is weird. If the dark theme is on, then `defaults read -g AppleInterfaceStyle` returns "Dark". If the light
|
||||
# theme is on, then trying to read this key fails and returns an error instead.
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["defaults", "read", "-g", "AppleInterfaceStyle"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return "Dark" in result.stdout
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_dark_theme_linux():
|
||||
try:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["gsettings", "get", "org.gnome.desktop.interface", "gtk-theme"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
# Looking for *not* "Light", because I want any theme that isn't light to be dark. An example of this is my own
|
||||
# KDE Plasma setup on my desktop, where I use the "Breeze" GTK theme and want dark NUSGet to be used in that
|
||||
# case.
|
||||
return not "light" in result.stdout.lower()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_dark_theme():
|
||||
# First, check for an environment variable overriding the theme, and use that if it exists.
|
||||
try:
|
||||
if os.environ["THEME"].lower() == "light":
|
||||
return False
|
||||
elif os.environ["THEME"].lower() == "dark":
|
||||
return True
|
||||
else:
|
||||
print(f"Unknown theme specified: \"{os.environ['THEME']}\"")
|
||||
except KeyError:
|
||||
pass
|
||||
# If the theme wasn't overridden, then check the current system theme.
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
return is_dark_theme_windows()
|
||||
elif system == "Darwin":
|
||||
return is_dark_theme_macos()
|
||||
else:
|
||||
return is_dark_theme_linux()
|
||||
Reference in New Issue
Block a user