Finish asset gui logic and argument parse module.
This commit is contained in:
@@ -0,0 +1,151 @@
|
|||||||
|
import logging
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from core_lib.game.library import get_natives_platform_info
|
||||||
|
|
||||||
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
|
def is_argument_allowed(argument: dict | str, allow_features: dict=dict) -> bool:
|
||||||
|
"""
|
||||||
|
Check if argument is allowed or not.
|
||||||
|
:param argument:
|
||||||
|
:param allow_features: Should be like {key: value}
|
||||||
|
:return:
|
||||||
|
allowed: bool
|
||||||
|
"""
|
||||||
|
if isinstance(argument, str):
|
||||||
|
return True
|
||||||
|
|
||||||
|
rules = argument.get('rules', {})
|
||||||
|
|
||||||
|
_, platform_rule_name, _ = get_natives_platform_info()
|
||||||
|
|
||||||
|
allowed = False
|
||||||
|
|
||||||
|
if not rules:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for rule in rules:
|
||||||
|
rule_action = rule.get("action", None)
|
||||||
|
os_section = rule.get("os", {})
|
||||||
|
rule_os = os_section.get("name", None)
|
||||||
|
features_rule = rule.get("features", None)
|
||||||
|
|
||||||
|
if rule_os is not None and platform_rule_name != rule_os:
|
||||||
|
continue
|
||||||
|
|
||||||
|
matched = True
|
||||||
|
|
||||||
|
# Check feature dict
|
||||||
|
if features_rule:
|
||||||
|
for key, expected in features_rule.items():
|
||||||
|
if allow_features.get(key, False) != expected:
|
||||||
|
matched = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
if rule_action == "allow":
|
||||||
|
allowed = True
|
||||||
|
elif rule_action == "disallow":
|
||||||
|
allowed = False
|
||||||
|
else:
|
||||||
|
logger.warning(f"Unknown action {rule_action}\n")
|
||||||
|
|
||||||
|
return allowed
|
||||||
|
|
||||||
|
def replace_placeholders(argument, placeholders: dict) -> str:
|
||||||
|
if argument:
|
||||||
|
for key, value in placeholders.items():
|
||||||
|
argument = argument.replace("${" + key + "}", "" if value is None else str(value))
|
||||||
|
|
||||||
|
return argument
|
||||||
|
|
||||||
|
class ArgumentMappings:
|
||||||
|
def __init__(self, player_name="Player", version_name="unknown",
|
||||||
|
game_directory=None, assets_root=None, assets_index_name=None,
|
||||||
|
auth_uuid="00000001-0002-0003-0004-000000000005",
|
||||||
|
auth_access_token="", clientid=None, auth_xuid=None,
|
||||||
|
user_type=None, version_type=None, natives_directory=None,
|
||||||
|
launcher_name=None, launcher_version=None,
|
||||||
|
classpath=None, classpath_separator=None, library_directory=None):
|
||||||
|
self.player_name = player_name
|
||||||
|
self.version_name = version_name
|
||||||
|
self.game_directory = game_directory
|
||||||
|
self.assets_root = assets_root
|
||||||
|
self.assets_index_name = assets_index_name
|
||||||
|
self.auth_uuid = auth_uuid
|
||||||
|
self.auth_access_token = auth_access_token
|
||||||
|
self.clientid = clientid
|
||||||
|
self.auth_xuid = auth_xuid
|
||||||
|
self.user_type = user_type
|
||||||
|
self.version_type = version_type
|
||||||
|
self.natives_directory = natives_directory
|
||||||
|
self.launcher_name = launcher_name
|
||||||
|
self.launcher_version = launcher_version
|
||||||
|
self.classpath = classpath
|
||||||
|
self.classpath_separator = classpath_separator
|
||||||
|
self.library_directory = library_directory
|
||||||
|
|
||||||
|
def generate_placeholders(self):
|
||||||
|
return {
|
||||||
|
"player_name": self.player_name,
|
||||||
|
"version_name": self.version_name,
|
||||||
|
"game_directory": self.game_directory,
|
||||||
|
"assets_root": self.assets_root,
|
||||||
|
"assets_index_name": self.assets_index_name,
|
||||||
|
"auth_uuid": self.auth_uuid,
|
||||||
|
"auth_access_token": self.auth_access_token,
|
||||||
|
"auth_player_name": self.player_name,
|
||||||
|
"clientid": self.clientid,
|
||||||
|
"auth_xuid": self.auth_xuid,
|
||||||
|
"user_type": self.user_type,
|
||||||
|
"version_type": self.version_type,
|
||||||
|
"natives_directory": self.natives_directory,
|
||||||
|
"launcher_name": self.launcher_name,
|
||||||
|
"launcher_version": self.launcher_version,
|
||||||
|
"classpath": self.classpath,
|
||||||
|
"classpath_separator": self.classpath_separator,
|
||||||
|
"library_directory": self.library_directory
|
||||||
|
}
|
||||||
|
|
||||||
|
def convert_argument_to_multiple_string(argument: str | dict) -> list[str]:
|
||||||
|
if isinstance(argument, str):
|
||||||
|
return [argument]
|
||||||
|
|
||||||
|
if isinstance(argument, dict):
|
||||||
|
value = argument.get("value", None)
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [str(item) for item in value] # ensure all item are string
|
||||||
|
elif isinstance(value, str):
|
||||||
|
return [value]
|
||||||
|
|
||||||
|
raise TypeError("Argument must be of type str or dict")
|
||||||
|
|
||||||
|
def parse_and_generate_arguments(arguments: list | str, mappings: ArgumentMappings, features: dict=dict) -> list[str]:
|
||||||
|
parsed_arguments = []
|
||||||
|
|
||||||
|
if isinstance(arguments, str):
|
||||||
|
# Convert legacy argument into a list type
|
||||||
|
arguments = shlex.split(arguments)
|
||||||
|
|
||||||
|
placeholders = mappings.generate_placeholders()
|
||||||
|
|
||||||
|
for argument in arguments:
|
||||||
|
# Check rules
|
||||||
|
if not is_argument_allowed(argument, allow_features=features):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Each argument (section) may contain multiple items, convert it to list before replace placeholder
|
||||||
|
converted = convert_argument_to_multiple_string(argument)
|
||||||
|
|
||||||
|
for item in converted:
|
||||||
|
parsed = replace_placeholders(item, placeholders)
|
||||||
|
parsed_arguments.append(parsed)
|
||||||
|
|
||||||
|
return parsed_arguments
|
||||||
|
|
||||||
|
|
||||||
+12
-2
@@ -13,6 +13,8 @@ from core_lib.game.version_info import find_useful_part_in_specific_version_mani
|
|||||||
|
|
||||||
MOJANG_RESOURCE_ENDPOINT = "https://resources.download.minecraft.net/{beginning}/{hash}"
|
MOJANG_RESOURCE_ENDPOINT = "https://resources.download.minecraft.net/{beginning}/{hash}"
|
||||||
|
|
||||||
|
ASSET_DOWNLOAD_MAX_WORKERS = 20
|
||||||
|
|
||||||
logger = logging.getLogger("Launcher.CoreLib")
|
logger = logging.getLogger("Launcher.CoreLib")
|
||||||
|
|
||||||
def fetch_asset_manifest(target_version_manifest: dict, raw=False) -> tuple[dict | bytes, str]:
|
def fetch_asset_manifest(target_version_manifest: dict, raw=False) -> tuple[dict | bytes, str]:
|
||||||
@@ -84,7 +86,7 @@ def fetch_and_save_asset_manifest(target_version_manifest: dict, destination: Pa
|
|||||||
with file.path.open("wb") as f:
|
with file.path.open("wb") as f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
||||||
if sha1 is not None and not file.verify_sha1(sha1):
|
if sha1 and not file.verify_sha1(sha1):
|
||||||
raise HashMismatchException(
|
raise HashMismatchException(
|
||||||
"sha1",
|
"sha1",
|
||||||
file.sha1,
|
file.sha1,
|
||||||
@@ -94,6 +96,7 @@ def fetch_and_save_asset_manifest(target_version_manifest: dict, destination: Pa
|
|||||||
|
|
||||||
return json.loads(data.decode("utf-8"))
|
return json.loads(data.decode("utf-8"))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
raise AssetManifestSaveException(
|
raise AssetManifestSaveException(
|
||||||
"Error saving version manifest:\n{}".format(e)
|
"Error saving version manifest:\n{}".format(e)
|
||||||
)
|
)
|
||||||
@@ -164,7 +167,14 @@ def download_assets_and_copy(assets: list[FileObject], legacy_assets: list[dict]
|
|||||||
files: list[FileObject] (files that download failed)
|
files: list[FileObject] (files that download failed)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fails , _ = download_multiple_files(assets, progress_callback=progress_callback)
|
fails , _ = download_multiple_files(assets, progress_callback=progress_callback,
|
||||||
|
max_workers=ASSET_DOWNLOAD_MAX_WORKERS)
|
||||||
|
|
||||||
|
if fails:
|
||||||
|
logger.warning(
|
||||||
|
"Download failed for assets: \n"
|
||||||
|
+"\n".join([asset.posix_path for asset in fails])
|
||||||
|
)
|
||||||
|
|
||||||
for asset in legacy_assets:
|
for asset in legacy_assets:
|
||||||
target = asset.get("target")
|
target = asset.get("target")
|
||||||
|
|||||||
@@ -413,6 +413,7 @@ def fetch_and_save_version_manifest(dest: Path, target_version: str=None, root_m
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
raise VersionManifestSaveException(
|
raise VersionManifestSaveException(
|
||||||
"Error saving version manifest:\n{}".format(e)
|
"Error saving version manifest:\n{}".format(e)
|
||||||
)
|
)
|
||||||
+147
-14
@@ -2,6 +2,7 @@ import json
|
|||||||
import textwrap
|
import textwrap
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
import traceback
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, QObject, Signal, Slot
|
from PySide6.QtCore import Qt, QObject, Signal, Slot
|
||||||
@@ -11,7 +12,9 @@ from PySide6.QtWidgets import QPushButton, QVBoxLayout, QDialog, QPlainTextEdit,
|
|||||||
from core_lib.common.common import download_file, FileObject, download_multiple_files
|
from core_lib.common.common import download_file, FileObject, download_multiple_files
|
||||||
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
|
from core_lib.common.exception import VersionNotSelectedException, UnsupportedPlatformException, \
|
||||||
NativeResolveException, DownloadException, DependencyException, VersionManifestFetchException, \
|
NativeResolveException, DownloadException, DependencyException, VersionManifestFetchException, \
|
||||||
VersionManifestSaveException, NoSpecifiedVersionKeyException
|
VersionManifestSaveException, NoSpecifiedVersionKeyException, AssetManifestFetchException, \
|
||||||
|
AssetManifestSaveException, AssetDataKeyNotFoundException
|
||||||
|
from core_lib.game.asset import fetch_and_save_asset_manifest, collect_assets_from_manifest, download_assets_and_copy
|
||||||
from core_lib.game.library import collect_native_artifacts, download_libraries, unzip_natives, collect_library_artifacts
|
from core_lib.game.library import collect_native_artifacts, download_libraries, unzip_natives, collect_library_artifacts
|
||||||
|
|
||||||
from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest,
|
from core_lib.game.version_info import (find_useful_part_in_specific_version_manifest,
|
||||||
@@ -48,6 +51,10 @@ class TestDialog(QDialog):
|
|||||||
self.button_4.setObjectName("ActionButton")
|
self.button_4.setObjectName("ActionButton")
|
||||||
self.button_4.clicked.connect(self.load_all_version)
|
self.button_4.clicked.connect(self.load_all_version)
|
||||||
|
|
||||||
|
self.button_5 = QPushButton("Download assets")
|
||||||
|
self.button_5.setObjectName("ActionButton")
|
||||||
|
self.button_5.clicked.connect(self.download_assets)
|
||||||
|
|
||||||
self.ver_dropdown = QComboBox()
|
self.ver_dropdown = QComboBox()
|
||||||
self.ver_dropdown.setObjectName("Dropdown")
|
self.ver_dropdown.setObjectName("Dropdown")
|
||||||
|
|
||||||
@@ -70,6 +77,7 @@ class TestDialog(QDialog):
|
|||||||
self.right_layout.addWidget(self.button)
|
self.right_layout.addWidget(self.button)
|
||||||
self.right_layout.addWidget(self.button_2)
|
self.right_layout.addWidget(self.button_2)
|
||||||
self.right_layout.addWidget(self.button_3)
|
self.right_layout.addWidget(self.button_3)
|
||||||
|
self.right_layout.addWidget(self.button_5)
|
||||||
self.right_layout.addStretch()
|
self.right_layout.addStretch()
|
||||||
self.right_layout.addWidget(self.ver_type_dropdown)
|
self.right_layout.addWidget(self.ver_type_dropdown)
|
||||||
self.right_layout.addWidget(self.ver_dropdown)
|
self.right_layout.addWidget(self.ver_dropdown)
|
||||||
@@ -131,6 +139,10 @@ class TestDialog(QDialog):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.widget_signal.show_and_print_error_message.connect(
|
||||||
|
self.show_and_print_error
|
||||||
|
)
|
||||||
|
|
||||||
self.widget_signal.show_warning_message.connect(
|
self.widget_signal.show_warning_message.connect(
|
||||||
lambda msg: messagebox.warning(
|
lambda msg: messagebox.warning(
|
||||||
self.parent,
|
self.parent,
|
||||||
@@ -185,6 +197,7 @@ class TestDialog(QDialog):
|
|||||||
update_vers_dropdown = Signal(list)
|
update_vers_dropdown = Signal(list)
|
||||||
update_types_dropdown = Signal(list)
|
update_types_dropdown = Signal(list)
|
||||||
show_error_message = Signal(str)
|
show_error_message = Signal(str)
|
||||||
|
show_and_print_error_message = Signal(dict)
|
||||||
show_warning_message = Signal(str)
|
show_warning_message = Signal(str)
|
||||||
show_info_message = Signal(str)
|
show_info_message = Signal(str)
|
||||||
enable_widget = Signal(object)
|
enable_widget = Signal(object)
|
||||||
@@ -227,6 +240,25 @@ class TestDialog(QDialog):
|
|||||||
if isinstance(event, threading.Event):
|
if isinstance(event, threading.Event):
|
||||||
event.set()
|
event.set()
|
||||||
|
|
||||||
|
@Slot(dict)
|
||||||
|
def show_and_print_error(self, context: dict):
|
||||||
|
error = context.get("error", None)
|
||||||
|
title = context.get("title", "Error")
|
||||||
|
message = context.get("message", "No content")
|
||||||
|
|
||||||
|
if isinstance(error, list):
|
||||||
|
self.app.logger.error("".join(error))
|
||||||
|
elif isinstance(error, str):
|
||||||
|
self.app.logger.error(error)
|
||||||
|
elif isinstance(error, Exception):
|
||||||
|
self.app.logger.exception(error)
|
||||||
|
|
||||||
|
messagebox.error(
|
||||||
|
self.parent,
|
||||||
|
title,
|
||||||
|
message=message
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_progress(value):
|
def get_progress(value):
|
||||||
width = 30
|
width = 30
|
||||||
@@ -326,8 +358,12 @@ class TestDialog(QDialog):
|
|||||||
""")
|
""")
|
||||||
self.output.setPlainText(content)
|
self.output.setPlainText(content)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
"Failed to run test. Details: {}".format(e)
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Test Error",
|
||||||
|
"message": "Failed to run test."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.button.setDisabled(False)
|
self.button.setDisabled(False)
|
||||||
@@ -362,7 +398,25 @@ class TestDialog(QDialog):
|
|||||||
return
|
return
|
||||||
|
|
||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
target=self._download_libraries_process,
|
target=self._download_libraries,
|
||||||
|
daemon=True,
|
||||||
|
args=(ver,)
|
||||||
|
)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def download_assets(self):
|
||||||
|
self.button_5.setDisabled(True)
|
||||||
|
|
||||||
|
self.output.clear()
|
||||||
|
|
||||||
|
ver = self.get_selected_version()
|
||||||
|
|
||||||
|
if not ver:
|
||||||
|
self.widget_signal.enable_widget.emit(self.button_5)
|
||||||
|
return
|
||||||
|
|
||||||
|
thread = threading.Thread(
|
||||||
|
target=self._download_assets,
|
||||||
daemon=True,
|
daemon=True,
|
||||||
args=(ver,)
|
args=(ver,)
|
||||||
)
|
)
|
||||||
@@ -444,8 +498,12 @@ class TestDialog(QDialog):
|
|||||||
"Unable to save version manifest: {}".format(e.user_message),
|
"Unable to save version manifest: {}".format(e.user_message),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
"An unexpected error occurred while caching version manifest: {}".format(e),
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Cache Error",
|
||||||
|
"message": "An unexpected error occurred while caching version manifest."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return True if data else False, data
|
return True if data else False, data
|
||||||
@@ -462,8 +520,12 @@ class TestDialog(QDialog):
|
|||||||
version_manifest = json.loads(path.read_text())
|
version_manifest = json.loads(path.read_text())
|
||||||
return version_manifest
|
return version_manifest
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
"Unable to read version manifest:\n{}".format(e),
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Cache Error",
|
||||||
|
"message": "Unable to read version manifest."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -542,13 +604,17 @@ class TestDialog(QDialog):
|
|||||||
"Unable to download client while downloading it: {}".format(e.user_message),
|
"Unable to download client while downloading it: {}".format(e.user_message),
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
"Unable to download client: {}".format(e),
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Download Error",
|
||||||
|
"message": "Unable to download client, an unexpected error occurred."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_2)
|
self.widget_signal.enable_widget.emit(self.button_2)
|
||||||
|
|
||||||
def _download_libraries_process(self, ver: str):
|
def _download_libraries(self, ver: str):
|
||||||
output_dir = Path(self.app.work_dir, "libraries")
|
output_dir = Path(self.app.work_dir, "libraries")
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
@@ -610,13 +676,80 @@ class TestDialog(QDialog):
|
|||||||
f"{e.user_message}"
|
f"{e.user_message}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.widget_signal.show_error_message.emit(
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
"Unable to collect library artifacts. An unknown error occurred:\n"
|
{
|
||||||
f"{str(e) + f"\n{type(e)}"}"
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Download Error",
|
||||||
|
"message": "Unable to collect library artifacts. An unknown error occurred."
|
||||||
|
}
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self.widget_signal.enable_widget.emit(self.button_3)
|
self.widget_signal.enable_widget.emit(self.button_3)
|
||||||
|
|
||||||
|
def _download_assets(self, ver: str):
|
||||||
|
try:
|
||||||
|
self.widget_signal.clean_output.emit()
|
||||||
|
|
||||||
|
ver_data = self.get_cached_version_manifest(ver)
|
||||||
|
|
||||||
|
if not ver_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
_, asset_index, _, _, _, _, _ = find_useful_part_in_specific_version_manifest(ver_data)
|
||||||
|
|
||||||
|
asset_id = asset_index.get("id")
|
||||||
|
assets_dir = Path(self.app.work_dir, "assets")
|
||||||
|
index_path = Path(assets_dir, "indexes", f"{asset_id}.json")
|
||||||
|
|
||||||
|
# Download and save asset manifest
|
||||||
|
asset_manifest = fetch_and_save_asset_manifest(
|
||||||
|
ver_data,
|
||||||
|
index_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Download assets files
|
||||||
|
assets, legacy_assets = collect_assets_from_manifest(
|
||||||
|
asset_id,
|
||||||
|
asset_manifest,
|
||||||
|
assets_dir,
|
||||||
|
)
|
||||||
|
|
||||||
|
fails = download_assets_and_copy(
|
||||||
|
assets,
|
||||||
|
legacy_assets,
|
||||||
|
progress_callback=self.download_signal.progress.emit,
|
||||||
|
)
|
||||||
|
|
||||||
|
if fails:
|
||||||
|
self.widget_signal.show_error_message.emit(
|
||||||
|
"Some assets failed to download:\n"
|
||||||
|
+ "\n".join(file.posix_path for file in fails)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.download_signal.log.emit("All necessary assets were downloaded.")
|
||||||
|
except AssetManifestFetchException as e:
|
||||||
|
self.widget_signal.show_error_message.emit(
|
||||||
|
f"Unable to fetch asset manifest.\n{e.user_message}"
|
||||||
|
)
|
||||||
|
except AssetManifestSaveException as e:
|
||||||
|
self.widget_signal.show_error_message.emit(
|
||||||
|
f"Unable to save asset manifest.\n{e.user_message}"
|
||||||
|
)
|
||||||
|
except AssetDataKeyNotFoundException as e:
|
||||||
|
self.widget_signal.show_error_message.emit(
|
||||||
|
f"Unable to collect assets.\n{e.user_message}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.widget_signal.show_and_print_error_message.emit(
|
||||||
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Download Error",
|
||||||
|
"message": "Unable to download assets. An unknown error occurred."
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self.widget_signal.enable_widget.emit(self.button_5)
|
||||||
|
|
||||||
def redownload_files(self, files: list[FileObject]) -> tuple[bool, list[FileObject]]:
|
def redownload_files(self, files: list[FileObject]) -> tuple[bool, list[FileObject]]:
|
||||||
if not files:
|
if not files:
|
||||||
return True, []
|
return True, []
|
||||||
|
|||||||
Reference in New Issue
Block a user