3232}
3333
3434
35+ def _get_app_dir () -> Path :
36+ """
37+ Get the top-level application directory.
38+ In frozen PyInstaller onedir builds, ROOT_DIR points to _internal/,
39+ but the .exe lives one level up. This returns the correct dir.
40+ """
41+ if getattr (sys , 'frozen' , False ):
42+ return Path (sys .executable ).parent
43+ return ROOT_DIR
44+
45+
3546def get_current_version () -> str :
3647 """Read current app version from resources/version.txt."""
3748 try :
@@ -112,19 +123,47 @@ def check_for_update() -> dict | None:
112123
113124def download_update (download_url : str ) -> Path :
114125 """
115- Download update zip to ROOT_DIR /_update_download.zip.
126+ Download update zip to app_dir /_update_download.zip.
116127 Uses Downloader from update_libmpv for multi-threaded download.
117128 Returns path to downloaded file.
118129 """
119- target = ROOT_DIR / '_update_download.zip'
130+ import time as _time
131+
132+ app_dir = _get_app_dir ()
133+ target = app_dir / '_update_download.zip'
134+
135+ print ("=" * 50 )
136+ print (" Downloading application update..." )
137+ print ("=" * 50 )
138+ print (f"URL: { download_url } " )
139+ print (f"Target: { target } " )
140+ print ("" )
120141
121142 try :
122- from update_libmpv import Downloader
143+ from update_libmpv import Downloader , format_size
123144 downloader = Downloader (download_url , str (target ))
124- downloader .download ()
145+
146+ # Get file size before download
147+ req = urllib .request .Request (download_url , headers = {'User-Agent' : USER_AGENT }, method = 'HEAD' )
148+ try :
149+ with urllib .request .urlopen (req , timeout = 15 ) as resp :
150+ total = int (resp .info ().get ('Content-Length' , 0 ))
151+ if total > 0 :
152+ print (f"File size: { format_size (total )} " )
153+ except Exception :
154+ pass
155+
156+ print (f"Threads: { downloader .num_threads } " )
157+ print ("Downloading..." )
158+ print ("" )
159+
160+ duration = downloader .download ()
161+
162+ print ("" )
163+ print (f"Download completed in { duration :.1f} s" )
125164 except ImportError :
126165 # Fallback: simple download
127- print (f"Downloading update from { download_url } ..." )
166+ print ("Using single-thread fallback ..." )
128167 req = urllib .request .Request (download_url , headers = {'User-Agent' : USER_AGENT })
129168 with urllib .request .urlopen (req , timeout = 300 ) as response :
130169 with open (target , 'wb' ) as f :
@@ -137,6 +176,11 @@ def download_update(download_url: str) -> Path:
137176
138177 if not target .exists ():
139178 raise RuntimeError ("Download failed: file not found" )
179+
180+ size_mb = target .stat ().st_size / (1024 * 1024 )
181+ print (f"Saved: { target .name } ({ size_mb :.1f} MB)" )
182+ print ("" )
183+ print ("Creating updater script..." )
140184 return target
141185
142186
@@ -145,44 +189,48 @@ def create_updater_script(zip_path: Path, new_version: str) -> Path:
145189 Generate _updater.bat that waits for app exit, extracts zip,
146190 copies files (skipping protected ones), restarts app, self-deletes.
147191 """
148- bat_path = ROOT_DIR / '_updater.bat'
192+ app_dir = _get_app_dir ()
193+ bat_path = app_dir / '_updater.bat'
149194 exe_name = _get_exe_name ()
195+ exe_path = _get_exe_path ()
150196 current_pid = os .getpid ()
151197
152- # Build exclusion filter for findstr
153- # Each protected item on its own line for findstr matching
154- protected_patterns = '\\ n' .join ([
155- f"\\ \\ { item } " if not item .endswith (('.exe' , '.dll' , '.ini' , '.version' ))
156- else f"\\ \\ { item } "
157- for item in PROTECTED_ITEMS
158- ])
159-
160198 script = f"""@echo off
161199chcp 65001 >nul
162200setlocal EnableDelayedExpansion
163201
164202set "APP_DIR=%~dp0"
165- set "EXE_NAME= { exe_name } "
203+ set "EXE_PATH= { exe_path } "
166204set "PID={ current_pid } "
167205set "ZIP_PATH={ zip_path } "
168206set "TEMP_DIR=%APP_DIR%_update_temp"
169207set "VERSION={ new_version } "
208+ set "LOG=%APP_DIR%_update_log.txt"
209+
210+ echo [%date% %time%] Update started > "%LOG%"
211+ echo APP_DIR=%APP_DIR% >> "%LOG%"
212+ echo EXE_PATH=%EXE_PATH% >> "%LOG%"
213+ echo ZIP_PATH=%ZIP_PATH% >> "%LOG%"
170214
171215REM --- Wait for application to exit ---
216+ echo Waiting for PID %PID% to exit... >> "%LOG%"
172217:wait_loop
173218tasklist /FI "PID eq %PID%" 2>nul | find "%PID%" >nul
174219if not errorlevel 1 (
175220 timeout /t 1 /nobreak >nul
176221 goto wait_loop
177222)
223+ echo Process exited. >> "%LOG%"
178224
179225REM --- Small delay for file handles to release ---
180- timeout /t 2 /nobreak >nul
226+ timeout /t 3 /nobreak >nul
181227
182228REM --- Extract zip to temp directory ---
183229if exist "%TEMP_DIR%" rmdir /s /q "%TEMP_DIR%"
184230mkdir "%TEMP_DIR%" 2>nul
231+ echo Extracting zip... >> "%LOG%"
185232powershell -Command "Expand-Archive -Path '%ZIP_PATH%' -DestinationPath '%TEMP_DIR%' -Force"
233+ echo Extraction done. >> "%LOG%"
186234
187235REM --- Find the actual content root (may be nested in a subfolder) ---
188236set "SOURCE_DIR=%TEMP_DIR%"
@@ -193,76 +241,83 @@ def create_updater_script(zip_path: Path, new_version: str) -> Path:
193241for /d %%D in ("%TEMP_DIR%\\ *") do (
194242 set /a SUBFOLDER_COUNT+=1
195243 set "LAST_SUBFOLDER=%%D"
244+ echo Found subfolder: %%D >> "%LOG%"
196245)
197246
198- REM If exactly one subfolder and no files at root, use it as source
247+ REM Count files at root level
199248set "FILE_COUNT=0"
200- for %%F in ("%TEMP_DIR%\\ *.*") do set /a FILE_COUNT+=1
249+ for %%F in ("%TEMP_DIR%\\ *.*") do (
250+ set /a FILE_COUNT+=1
251+ echo Found root file: %%F >> "%LOG%"
252+ )
201253
202- if % SUBFOLDER_COUNT% equ 1 if % FILE_COUNT% equ 0 (
254+ if ! SUBFOLDER_COUNT! equ 1 if ! FILE_COUNT! equ 0 (
203255 set "SOURCE_DIR=!LAST_SUBFOLDER!"
256+ echo Using subfolder as source: !SOURCE_DIR! >> "%LOG%"
204257)
205258
206- REM --- Copy files, skipping protected items ---
207- xcopy "!SOURCE_DIR!\\ *" "%APP_DIR%" /e /y /i /exclude:%~f0.exc >nul 2>&1
259+ echo SOURCE_DIR=!SOURCE_DIR! >> "%LOG%"
260+
261+ REM --- List source contents for debugging ---
262+ echo Source directory contents: >> "%LOG%"
263+ dir /b "!SOURCE_DIR!" >> "%LOG%" 2>&1
208264
209- REM If xcopy exclude doesn't work well, use robocopy as fallback
210- robocopy "!SOURCE_DIR!" "%APP_DIR%" /e /xf settings.ini ffmpeg.exe ffprobe.exe libmpv-2.dll libmpv.version _updater.bat /xd data _update_temp >nul 2>&1
265+ REM --- Copy all files using robocopy, excluding protected items ---
266+ echo Starting robocopy... >> "%LOG%"
267+ robocopy "!SOURCE_DIR!" "%APP_DIR%." /e /np /njh /njs /r:3 /w:2 /xf settings.ini ffmpeg.exe ffprobe.exe libmpv-2.dll libmpv.version _updater.bat _updater.bat.exc _update_log.txt /xd data _update_temp >> "%LOG%" 2>&1
268+ echo Robocopy exit code: !errorlevel! >> "%LOG%"
211269
212270REM --- Update version file ---
213- echo %VERSION%> "%APP_DIR%resources\\ version.txt"
271+ if exist "%APP_DIR%_internal\\ resources\\ version.txt" (
272+ echo %VERSION%> "%APP_DIR%_internal\\ resources\\ version.txt"
273+ echo Updated version in _internal\\ resources\\ version.txt >> "%LOG%"
274+ ) else if exist "%APP_DIR%resources\\ version.txt" (
275+ echo %VERSION%> "%APP_DIR%resources\\ version.txt"
276+ echo Updated version in resources\\ version.txt >> "%LOG%"
277+ )
214278
215279REM --- Cleanup ---
280+ echo Cleaning up... >> "%LOG%"
216281rmdir /s /q "%TEMP_DIR%" 2>nul
217282del /f /q "%ZIP_PATH%" 2>nul
218283
219284REM --- Restart application ---
220- start "" "%APP_DIR%%EXE_NAME%"
285+ echo Starting: %EXE_PATH% >> "%LOG%"
286+ start "" "%EXE_PATH%"
221287
222288REM --- Bring window to foreground ---
223289timeout /t 2 /nobreak >nul
224- powershell -Command "(New-Object -ComObject WScript.Shell).AppActivate('%EXE_NAME% ')" >nul 2>&1
290+ powershell -Command "(New-Object -ComObject WScript.Shell).AppActivate('{ exe_name } ')" >nul 2>&1
225291
226292REM --- Self-delete ---
293+ echo Update complete. >> "%LOG%"
227294del "%~f0.exc" 2>nul
228295(goto) 2>nul & del "%~f0"
229296"""
230297
231- # Write exclusion file for xcopy (one pattern per line)
232- exc_path = Path (str (bat_path ) + '.exc' )
233- exc_lines = [
234- 'settings.ini' ,
235- 'ffmpeg.exe' ,
236- 'ffprobe.exe' ,
237- 'libmpv-2.dll' ,
238- 'libmpv.version' ,
239- '_updater.bat' ,
240- '\\ data\\ ' ,
241- '\\ _update_temp\\ ' ,
242- ]
243- exc_path .write_text ('\n ' .join (exc_lines ), encoding = 'utf-8' )
244-
245298 bat_path .write_text (script , encoding = 'utf-8' )
246299 return bat_path
247300
248301
249302def launch_updater_and_exit (bat_path : Path ):
250303 """Launch the updater bat script hidden and quit the application."""
304+ app_dir = _get_app_dir ()
251305 subprocess .Popen (
252306 ['cmd' , '/c' , str (bat_path )],
253307 creationflags = subprocess .CREATE_NO_WINDOW ,
254308 close_fds = True ,
255- cwd = str (ROOT_DIR ),
309+ cwd = str (app_dir ),
256310 )
257311
258312
259313def cleanup_update_artifacts ():
260314 """Remove leftover update files from previous runs."""
315+ app_dir = _get_app_dir ()
261316 artifacts = [
262- ROOT_DIR / '_update_temp' ,
263- ROOT_DIR / '_update_download.zip' ,
264- ROOT_DIR / '_updater.bat' ,
265- ROOT_DIR / '_updater.bat.exc' ,
317+ app_dir / '_update_temp' ,
318+ app_dir / '_update_download.zip' ,
319+ app_dir / '_updater.bat' ,
320+ app_dir / '_updater.bat.exc' ,
266321 ]
267322 for path in artifacts :
268323 try :
@@ -276,8 +331,14 @@ def cleanup_update_artifacts():
276331
277332
278333def _get_exe_name () -> str :
279- """Get the name of the running executable."""
334+ """Get the name of the running executable (just filename) ."""
280335 if getattr (sys , 'frozen' , False ):
281336 return Path (sys .executable ).name
282- # Dev mode: return python script name
283337 return 'SP Video Courses Player.exe'
338+
339+
340+ def _get_exe_path () -> str :
341+ """Get the full path to the running executable."""
342+ if getattr (sys , 'frozen' , False ):
343+ return str (Path (sys .executable ))
344+ return str (ROOT_DIR / 'SP Video Courses Player.exe' )
0 commit comments