a0704efa3c
src/edit_popup.py: pass and reproduce the server-side edited-media layout
('edited_media/<media_id>/') when saving an edit, falling back to the flat
folder when no media id is available, so uploads land where the server expects.
src/get_playlists_v2.py: preserve every server field (audio, muted, description,
id, position, ...) when rewriting playlist items, instead of rebuilding fixed
dicts that silently dropped them. Web-link items keep their original http(s) url
and are never downloaded.
windows/pyi_runtime_hook.py: declare per-monitor DPI awareness before SDL/Kivy
initialise, so on a scaled display the window is not virtualised to a smaller
resolution (which left a black strip and mis-scaled media).
windows/build_win.bat: optional code signing step for PCs with Smart App Control
enabled, driven by KIWY_SIGN_PFX / KIWY_SIGN_PFX_PASSWORD or a local
kiwy_signing.pfx.
windows/build.spec, windows/README_WINDOWS_BUILD.md, windows/development-track.md:
build notes and manifest updates.
165 lines
6.6 KiB
Python
165 lines
6.6 KiB
Python
"""
|
|
PyInstaller Runtime Hook for Kiwy Signage Player
|
|
------------------------------------------------
|
|
Runs at startup of the packaged .exe to fix paths and environment.
|
|
Creates all necessary folders LOCAL to the executable's directory.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import platform
|
|
from pathlib import Path
|
|
|
|
|
|
def _set_process_dpi_awareness():
|
|
"""Declare per-monitor DPI awareness BEFORE SDL/Kivy initialize.
|
|
|
|
On a display scaled above 100% (e.g. 1920x1080 @ 125%), Windows
|
|
virtualizes a non-DPI-aware app to the scaled-down size (1536x864).
|
|
Kivy then sizes its content area to the virtualized resolution, leaving a
|
|
black strip on one side and making images/videos render at the wrong size.
|
|
Must run before any SDL window is created, so this lives in the runtime
|
|
hook (the first Python code that runs in the frozen app).
|
|
"""
|
|
try:
|
|
try:
|
|
aware = ctypes.c_int(2) # PROCESS_PER_MONITOR_DPI_AWARE_V2
|
|
ctypes.windll.shcore.SetProcessDpiAwareness(aware)
|
|
return
|
|
except Exception:
|
|
pass
|
|
try:
|
|
ctypes.windll.user32.SetProcessDPIAware()
|
|
return
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
if platform.system() == 'Windows':
|
|
import ctypes
|
|
_set_process_dpi_awareness()
|
|
|
|
# ── IMPORTANT: Set Windows environment BEFORE any Kivy code runs ──
|
|
# This must happen before main.py's top-level code executes, because
|
|
# main.py sets SDL_VIDEODRIVER=wayland,x11,dummy which would crash on Windows.
|
|
os.environ['SDL_VIDEODRIVER'] = 'windows'
|
|
os.environ['SDL_AUDIODRIVER'] = 'directsound'
|
|
os.environ['KIVY_WINDOW'] = 'sdl2'
|
|
os.environ['KIVY_GL_BACKEND'] = 'angle_sdl2'
|
|
os.environ['KIVY_VIDEO'] = 'ffpyplayer'
|
|
os.environ['KIVY_AUDIO'] = 'ffpyplayer'
|
|
os.environ['FFPYPLAYER_CODECS'] = 'h264,h265,vp9,vp8'
|
|
os.environ['SDL_VIDEO_ALLOW_SCREENSAVER'] = '0'
|
|
os.environ['KIVY_NO_FILELOG'] = '1'
|
|
os.environ['KIVY_INPUTPROVIDERS'] = '' # Let Kivy auto-detect on Windows
|
|
# Use native physical pixels (fixes black strip on DPI-scaled displays).
|
|
os.environ.setdefault('SDL_VIDEO_HIGHDPI', '1')
|
|
|
|
# ── Capture ALL early output to a crash log ─────────────────────────
|
|
# Ensure we catch any exception that happens before Logger is available.
|
|
_startup_log_path = None
|
|
try:
|
|
_exe_dir = Path(sys.executable).parent
|
|
_startup_log_path = _exe_dir / 'logs' / 'startup_crash.log'
|
|
(_startup_log_path.parent).mkdir(parents=True, exist_ok=True)
|
|
with open(_startup_log_path, 'w') as _f:
|
|
_f.write("pyi_runtime_hook.py started\n")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
def _setup_paths():
|
|
"""Ensure the app can find its bundled files at runtime.
|
|
|
|
All data folders (config, media, playlists, logs) are created
|
|
LOCAL to the executable's directory — NOT in %%APPDATA%%.
|
|
"""
|
|
# In PyInstaller, sys.executable is the .exe path.
|
|
# sys._MEIPASS is the extraction directory (i.e. _internal/ folder).
|
|
exe_dir = Path(sys.executable).parent
|
|
internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir))
|
|
|
|
# ── Change cwd to _internal so Builder.load_file('signage_player.kv')
|
|
# and other relative file references from main.py resolve ─────
|
|
os.chdir(str(internal_dir))
|
|
|
|
# Add bundled src directory to Python path
|
|
src_dir = str(internal_dir / 'src')
|
|
if os.path.isdir(src_dir) and src_dir not in sys.path:
|
|
sys.path.insert(0, src_dir)
|
|
|
|
# Add internal directory for config/media/playlists access
|
|
if str(internal_dir) not in sys.path:
|
|
sys.path.insert(0, str(internal_dir))
|
|
|
|
# ── Local folders next to the .exe ──────────────────────────────
|
|
# All data lives in the SAME folder as the executable so the user
|
|
# can copy/move the whole directory and everything still works.
|
|
os.environ['KIWY_DATA_DIR'] = str(exe_dir)
|
|
|
|
# Set KIVY_HOME to a local .kivy folder next to the .exe
|
|
kivy_home = exe_dir / '.kivy'
|
|
os.environ.setdefault('KIVY_HOME', str(kivy_home))
|
|
kivy_home.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Create local data folders next to the .exe
|
|
for sub in ['config', 'config/resources', 'media', 'playlists', 'logs']:
|
|
(exe_dir / sub).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def _copy_bundled_resources():
|
|
"""Copy bundled resource/config files to the local folders on first run."""
|
|
exe_dir = Path(sys.executable).parent
|
|
internal_dir = Path(getattr(sys, '_MEIPASS', exe_dir))
|
|
|
|
# Files to copy (source in bundle -> destination next to .exe)
|
|
files_to_copy = [
|
|
('config/app_config.json', 'config/app_config.json'),
|
|
('config/resources/access-card.png', 'config/resources/access-card.png'),
|
|
('config/resources/arrow.png', 'config/resources/arrow.png'),
|
|
('config/resources/backward.png', 'config/resources/backward.png'),
|
|
('config/resources/card-checked.png', 'config/resources/card-checked.png'),
|
|
('config/resources/edit-pen.png', 'config/resources/edit-pen.png'),
|
|
('config/resources/exit.png', 'config/resources/exit.png'),
|
|
('config/resources/forward.png', 'config/resources/forward.png'),
|
|
('config/resources/intro1.mp4', 'config/resources/intro1.mp4'),
|
|
('config/resources/pause.png', 'config/resources/pause.png'),
|
|
('config/resources/pencil.png', 'config/resources/pencil.png'),
|
|
('config/resources/play.png', 'config/resources/play.png'),
|
|
('config/resources/settings.png', 'config/resources/settings.png'),
|
|
]
|
|
|
|
for src_rel, dest_rel in files_to_copy:
|
|
src_path = internal_dir / src_rel
|
|
dest_path = exe_dir / dest_rel
|
|
if src_path.is_file() and not dest_path.exists():
|
|
try:
|
|
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
import shutil
|
|
shutil.copy2(str(src_path), str(dest_path))
|
|
except Exception:
|
|
pass # Non-critical; app can still run
|
|
|
|
|
|
# ── Wrap everything in try/except to capture early crashes ──────────
|
|
try:
|
|
_setup_paths()
|
|
_copy_bundled_resources()
|
|
# If we reach here, the hook finished successfully
|
|
try:
|
|
with open(_startup_log_path, 'a') as _f:
|
|
_f.write("pyi_runtime_hook.py completed successfully\n")
|
|
except Exception:
|
|
pass
|
|
except Exception as _hook_exc:
|
|
import traceback as _tb
|
|
try:
|
|
with open(_startup_log_path, 'a') as _f:
|
|
_f.write(f"pyi_runtime_hook.py CRASHED: {_hook_exc}\n")
|
|
_tb.print_exc(file=_f)
|
|
except Exception:
|
|
pass
|
|
raise # Re-raise so the .exe still fails visibly
|