mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-06-08 03:37:44 +02:00
Sync from development - prepare for v0.5.0.3
This commit is contained in:
@@ -4,6 +4,7 @@ list of pending manual download items by lax filename comparison.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
@@ -106,6 +107,14 @@ class DownloadWatcherService:
|
||||
logger.debug(f"Candidate dot-normalized match: {path.name} -> {expected_name}")
|
||||
self._debounce_and_emit(path, item)
|
||||
return
|
||||
# Some modlist metadata stores filenames with a leading numeric prefix
|
||||
# (e.g. "1_filename.zip") that is absent from the browser-saved file.
|
||||
for expected_name, item in self._pending_exact:
|
||||
stripped = re.sub(r'^\d+_', '', expected_name)
|
||||
if stripped != expected_name and stripped == candidate_name:
|
||||
logger.debug(f"Candidate numeric-prefix match: {path.name} -> {expected_name}")
|
||||
self._debounce_and_emit(path, item)
|
||||
return
|
||||
|
||||
def _debounce_and_emit(self, path: Path, item: dict) -> None:
|
||||
def _wait_and_emit():
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
@@ -380,6 +381,13 @@ class ManualDownloadManagerRuntimeMixin:
|
||||
stripped = name.lower().lstrip('.')
|
||||
if stripped != name.lower():
|
||||
exact = exact_map.get(stripped)
|
||||
if exact is None:
|
||||
# Numeric prefix normalization: engine may store filenames with a
|
||||
# leading numeric prefix (e.g. "1_filename.zip") absent from the
|
||||
# browser-saved file.
|
||||
stripped_num = re.sub(r'^\d+_', '', name.lower())
|
||||
if stripped_num != name.lower():
|
||||
exact = exact_map.get(stripped_num)
|
||||
if exact is None or exact in used_paths:
|
||||
continue
|
||||
used_paths.add(exact)
|
||||
|
||||
@@ -145,6 +145,8 @@ class ModlistServiceInstallationMixin:
|
||||
elif context.get('machineid'):
|
||||
cmd += ['-m', context['machineid']]
|
||||
cmd += ['-o', install_dir_str, '-d', download_dir_str]
|
||||
if context.get('skip_disk_check'):
|
||||
cmd.append('--skip-disk-check')
|
||||
|
||||
original_env_values = {
|
||||
'NEXUS_API_KEY': os.environ.get('NEXUS_API_KEY'),
|
||||
@@ -199,9 +201,10 @@ class ModlistServiceInstallationMixin:
|
||||
except (OSError, BrokenPipeError):
|
||||
return False
|
||||
|
||||
from jackify.backend.utils.cc_content_detector import is_cc_content_error, extract_cc_filename
|
||||
from jackify.backend.utils.cc_content_detector import is_cc_content_error, extract_cc_filename, is_creation_kit_missing_error
|
||||
import json as _json
|
||||
_cc_filename = None
|
||||
_ck_missing = False
|
||||
_pending_manual: list = []
|
||||
buffer = b''
|
||||
while True:
|
||||
@@ -263,6 +266,8 @@ class ModlistServiceInstallationMixin:
|
||||
output_callback(decoded)
|
||||
if _cc_filename is None and is_cc_content_error(decoded):
|
||||
_cc_filename = extract_cc_filename(decoded) or ""
|
||||
if not _ck_missing and is_creation_kit_missing_error(decoded):
|
||||
_ck_missing = True
|
||||
|
||||
if buffer:
|
||||
line = buffer.decode('utf-8', errors='replace')
|
||||
@@ -271,6 +276,8 @@ class ModlistServiceInstallationMixin:
|
||||
output_callback(decoded)
|
||||
if _cc_filename is None and is_cc_content_error(decoded):
|
||||
_cc_filename = extract_cc_filename(decoded) or ""
|
||||
if not _ck_missing and is_creation_kit_missing_error(decoded):
|
||||
_ck_missing = True
|
||||
|
||||
proc.wait()
|
||||
if proc.returncode != 0:
|
||||
@@ -285,6 +292,16 @@ class ModlistServiceInstallationMixin:
|
||||
output_callback(" - If specific files are still missing, search for and download them from the Creations menu.")
|
||||
output_callback(" - If problems persist, uninstall and reinstall Skyrim, then launch once to trigger the AE download.")
|
||||
output_callback(" - Note: Skyrim AE via Steam Family Sharing does not transfer DLC content.")
|
||||
if _ck_missing and output_callback:
|
||||
output_callback("")
|
||||
output_callback("[WARN] Creation Kit Files Missing")
|
||||
output_callback(" This modlist requires the Skyrim Special Edition Creation Kit.")
|
||||
output_callback(" - In Steam, search for 'Skyrim Special Edition: Creation Kit' and install it.")
|
||||
output_callback(" - Right-click it in Steam > Properties > Compatibility and set a Proton version.")
|
||||
output_callback(" - Click Play to launch the Creation Kit.")
|
||||
output_callback(" - When asked whether to unzip Scripts.zip, select NO.")
|
||||
output_callback(" - Once the Creation Kit opens successfully, close it.")
|
||||
output_callback(" - Re-run the modlist install in Jackify.")
|
||||
return False
|
||||
if output_callback:
|
||||
output_callback("Installation completed successfully")
|
||||
|
||||
@@ -369,15 +369,70 @@ class UpdateService:
|
||||
if progress_callback:
|
||||
progress_callback(downloaded_size, total_size)
|
||||
|
||||
# Nexus delivers a 7z archive — extract the AppImage before handing off
|
||||
if self._is_7z_archive(temp_file):
|
||||
logger.info("Downloaded file is a 7z archive, extracting AppImage")
|
||||
extracted = self._extract_appimage_from_7z(temp_file, update_dir, update_info.version)
|
||||
temp_file.unlink(missing_ok=True)
|
||||
if not extracted:
|
||||
logger.error("Failed to extract AppImage from 7z archive")
|
||||
return None
|
||||
temp_file = extracted
|
||||
|
||||
# Make executable
|
||||
temp_file.chmod(0o755)
|
||||
|
||||
|
||||
logger.info("Update downloaded successfully: %s from %s -> %s", update_info.version, update_info.source, temp_file)
|
||||
return temp_file
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to download update manually: {e}")
|
||||
return None
|
||||
|
||||
def _is_7z_archive(self, path: Path) -> bool:
|
||||
"""Detect 7z archive by magic bytes (37 7A BC AF 27 1C)."""
|
||||
try:
|
||||
with open(path, 'rb') as f:
|
||||
magic = f.read(6)
|
||||
return magic == b'7z\xbc\xaf\x27\x1c'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _get_bundled_7z_path(self) -> Optional[Path]:
|
||||
"""Return path to bundled 7z binary (AppImage or dev)."""
|
||||
import os
|
||||
candidates = []
|
||||
appdir = os.environ.get('APPDIR')
|
||||
if appdir:
|
||||
candidates.append(Path(appdir) / 'opt' / 'jackify' / 'tools' / '7z')
|
||||
candidates.append(Path(__file__).parent.parent.parent / 'tools' / '7z')
|
||||
for p in candidates:
|
||||
if p.exists() and os.access(p, os.X_OK):
|
||||
return p
|
||||
return None
|
||||
|
||||
def _extract_appimage_from_7z(self, archive: Path, dest_dir: Path, version: str) -> Optional[Path]:
|
||||
"""Extract Jackify.AppImage from a 7z archive into dest_dir."""
|
||||
seven_z = self._get_bundled_7z_path()
|
||||
if not seven_z:
|
||||
logger.error("Bundled 7z not found, cannot extract update archive")
|
||||
return None
|
||||
out_path = dest_dir / f"Jackify-{version}.AppImage"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(seven_z), 'e', str(archive), 'Jackify.AppImage', f'-o{dest_dir}', '-y'],
|
||||
capture_output=True, text=True, timeout=120
|
||||
)
|
||||
extracted = dest_dir / 'Jackify.AppImage'
|
||||
if result.returncode != 0 or not extracted.exists():
|
||||
logger.error("7z extraction failed (rc=%d): %s", result.returncode, result.stderr.strip())
|
||||
return None
|
||||
extracted.rename(out_path)
|
||||
logger.info("Extracted AppImage from archive: %s", out_path)
|
||||
return out_path
|
||||
except Exception as e:
|
||||
logger.error("Exception during 7z extraction: %s", e)
|
||||
return None
|
||||
|
||||
def apply_update(self, new_appimage_path: Path) -> bool:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user