Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,12 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v2.7.1
name: v2.7.1
tag_name: v2.7.2
name: v2.7.2
body: |
Allow users to change the maximum number of simultaneous downloads (default 12, can be set between 1 and 100).
Fix an issue where cancelling items wouldn't clear them from the total downloads and would require a new search to redownload.

Also clean up more of the codebase to make it more readable.
draft: false
prerelease: false
files: |
Expand Down
71 changes: 44 additions & 27 deletions PySN.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,17 @@
import yaml
import time
import sys
import xml.etree.ElementTree as ET
from tkinter import PhotoImage
from pathlib import Path
from fnmatch import fnmatch
from configparser import ConfigParser
import xml.etree.ElementTree as ET
from os import makedirs, path
from enum import Enum
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor

#Urls are http rather than https, so we have to disable the warning about unsecure requests.
requests.packages.urllib3.disable_warnings()

#Creates a directory for the game in the download path.
Expand Down Expand Up @@ -116,10 +117,13 @@ def __init__(self, *args, **kwargs):
self.title('Settings')
self.grid_rowconfigure((0, 1, 2, 3, 4, 5, 6, 7, 8), weight=1)
self.grid_columnconfigure((0, 1, 2, 3, 4, 5), weight=1)
self.temp_save, self.temp_rpcs3, self.temp_folder_format, self.temp_max_downloads = ConfigSettings.check_config()
if sys.platform == 'win32':
self.after(200, lambda: self.iconbitmap(resource_path("AphIcon.ico")))

self.temp_save, self.temp_rpcs3, self.temp_folder_format, self.temp_max_downloads = ConfigSettings.check_config()
self.char_limit = (self.register(self.validate_limit), '%P')


self.save_dir_label = customtkinter.CTkLabel(master=self, text='Download Update PKGs To This Folder:', justify = 'center', anchor='center')
self.save_dir_label.grid(row=0, column=1, columnspan=4, padx=5, pady=(20,0), sticky='sew')

Expand All @@ -144,8 +148,7 @@ def __init__(self, *args, **kwargs):

self.max_downloads_label = customtkinter.CTkLabel(master=self, text='Maximum Simultaneous Downloads (1-100):', anchor='center')
self.max_downloads_label.grid(row=6, column=1, columnspan=4, padx=0, pady=(0,0), sticky='sew')
char_limit = (self.register(self.validate_limit), '%P')
self.max_downloads_entry = customtkinter.CTkEntry(master=self, width=25, justify='center', validate='key', validatecommand=char_limit)
self.max_downloads_entry = customtkinter.CTkEntry(master=self, width=25, justify='center', validate='key', validatecommand=self.char_limit)
self.max_downloads_entry.grid(row=7, column=1, columnspan=4, padx=(230,230), pady=(0,30), sticky='new')
self.max_downloads_entry.insert(0, str(max_downloads))

Expand Down Expand Up @@ -203,11 +206,17 @@ def button_save(self):
save_dir = self.temp_save
else:
save_dir = self.temp_save + '/'

if self.temp_rpcs3.endswith('!') or self.temp_rpcs3.endswith('/'):
rpcs3_dir = self.temp_rpcs3
else:
rpcs3_dir = self.temp_rpcs3 + '/'
folder_format = 'id_name' if self.folder_format_toggle.get() == 'ID - Name' else 'name_id'

if self.folder_format_toggle.get() == 'ID - Name':
folder_format = 'id_name'
else:
folder_format = 'name_id'

ConfigSettings.save_config('w', save_dir , rpcs3_dir, folder_format, max_downloads)
self.destroy()

Expand Down Expand Up @@ -354,10 +363,6 @@ def clear_items(self):
class App(customtkinter.CTk):
def __init__(self):
super().__init__()
self.hash_executor = ThreadPoolExecutor(max_workers=4)
self.download_semaphore = threading.Semaphore(max_downloads)
self._search_thread = None
self.session = requests.Session()
self.running = True
self.protocol("WM_DELETE_WINDOW", self.on_closing)
self.geometry('760x640')
Expand All @@ -366,9 +371,6 @@ def __init__(self):
self.minsize(760, 0)
self.maxsize(760, 99999)
self.toplevel_window = None
self.total_download_size = 0
self.completed_download_size = 0
self._download_lock = threading.Lock()
self.grid_rowconfigure((0, 2, 3), weight=0)
self.grid_rowconfigure((1), weight=1)
self.grid_columnconfigure((0, 1, 2, 3, 4), weight=1)
Expand All @@ -379,6 +381,14 @@ def __init__(self):
icon = PhotoImage(file=resource_path("AphIcon.png"))
self.after(0, lambda: self.iconphoto(True, icon))

self.hash_executor = ThreadPoolExecutor(max_workers=4)
self.download_semaphore = threading.Semaphore(max_downloads)
self._search_thread = None
self.session = requests.Session()
self.total_download_size = 0
self.completed_download_size = 0
self._download_lock = threading.Lock()

self.entry = customtkinter.CTkEntry(master=self, placeholder_text='Enter Serial', width = 125)
self.entry.grid(row=0, column=0, padx=(4,2), pady=(6,0), sticky='ew')
self.combobox = customtkinter.CTkComboBox(master=self, values=['PlayStation 3', 'PlayStation 4', 'PlayStation Vita', 'PlayStation 5'], width = 125)
Expand Down Expand Up @@ -450,6 +460,7 @@ def hash_check_worker(self, index, fileloc, console, sha1):
else:
for chunk in iter(lambda: f.read(4 * 1024 * 1024), b''):
hash_obj.update(chunk)

match = (sha1.upper() == hash_obj.hexdigest().upper())

except Exception:
Expand Down Expand Up @@ -484,9 +495,7 @@ def update_total_progress_ui(self):
completed_mb = completed_bytes / 1024000

self.total_prog_bar.set(progress)
self.prog_status_label.configure(
text=f"{completed_mb:.2f}/{total_mb:.2f}MB"
)
self.prog_status_label.configure(text=f"{completed_mb:.2f}/{total_mb:.2f}MB")

self.after(50, self.update_total_progress_ui)

Expand All @@ -501,6 +510,7 @@ def is_shit_there(self, name, title_id, download_path, index, fileloc, console,
alt_fileloc = alt_download_path + '/' + path.basename(fileloc)
legacy_path = save_dir + console + '/' + title_id + ' ' + name
legacy_fileloc = legacy_path + '/' + path.basename(fileloc)

if path.exists(alt_fileloc):
download_path = alt_download_path
fileloc = alt_fileloc
Expand Down Expand Up @@ -568,6 +578,7 @@ def search_type(self):
elif sys.platform.startswith('darwin'):
yml_dir = Path.home() / 'Library' / 'Application Support' / 'rpcs3' / 'games.yml'
else: yml_dir = Path(rpcs3_dir) / 'config' / 'games.yml'

if os.path.isfile(yml_dir):
with open(yml_dir, 'r') as f:
file = yaml.safe_load(f)
Expand Down Expand Up @@ -639,6 +650,7 @@ def request_fw(self, console):
info_url = 'http://f' + locale + '01.ps5.update.playstation.net/update/ps5/official/'+ obf + '/list/'+ locale + '/updatelist.xml'
else:
info_url = 'https://f' + locale + '01.ps3.update.playstation.net/update/ps3/list/' + locale + '/ps3-updatelist.txt'

url_list.append(info_url)

def fetch_locale(info_url):
Expand Down Expand Up @@ -685,8 +697,10 @@ def search(self, title_id, console, root, game_name):
download_path = save_dir + console + '/[' + title_id + '] ' + name
else:
download_path = save_dir + console + '/' + name + ' [' + title_id + ']'

update_file = path.basename(url)
fileloc = (download_path + '/' + update_file)

self.after(0, lambda gn=name, tid=title_id, v=ver, u=url, c=console, us=update_size, s=sha1, dp=download_path, fl=fileloc:
(self.textbox.add_item(gn, tid, ' v' + v, u, c, us, s, len(self.textbox.dlbutton_list), dp, fl),
self.is_shit_there(gn, tid, dp, len(self.textbox.dlbutton_list) - 1, fl, c, s, us)))
Expand Down Expand Up @@ -733,8 +747,10 @@ def search_no_drm(self, title_id, console, root, game_name):
download_path = (save_dir + console + '/[' + title_id + '] ' + name_list[i])
else:
download_path = (save_dir + console + '/' + name_list[i] + ' [' + title_id + ']')

update_file = 'DRM-Free ' + path.basename(url_list[i])
fileloc = (download_path + '/' + update_file)

self.after(0, lambda gn=name_list[i], tid=title_id, v=version, u=url_list[i], c=console, us=update_size_list[i], s=sha1_list[i], dp=download_path, fl=fileloc:
(self.textbox.add_item(gn, tid, ' v' + v + ' DRM-Free', u, c, us, s, len(self.textbox.dlbutton_list), dp, fl),
self.is_shit_there(gn, tid, dp, len(self.textbox.dlbutton_list) - 1, fl, c, s, us)))
Expand Down Expand Up @@ -767,7 +783,6 @@ def search_ps3_fw(self, console):
update_url = self.session.get(url, stream=True, verify=False, timeout=10)
update_size = int(update_url.headers.get('Content-Length', 0))


#There's no hash available to check, so sha1 gets assigned N/A. Title ID becomes region for formatting. set paths and populate widgets.
sha1 = 'N/A'
title_id = region
Expand Down Expand Up @@ -858,7 +873,9 @@ def search_ps4_ps5_vita_fw(self, console):
self.after(0, lambda gn=game_name, tid=title_id, v=ver_list[i], u=url, c=console, us=update_size_list[i], s=sha1, dp=download_path, fl=fileloc:
(self.textbox.add_item(gn, tid, ' v' + v, u, c, us, s, len(self.textbox.dlbutton_list), dp, fl),
self.is_shit_there(gn, tid, dp, len(self.textbox.dlbutton_list) - 1, fl, c, s, us)))

i = i+1

else: self.after(0, lambda: self.textbox.add_item('Error Connecting to Server', '', '', '', '', 0, '', '', '', ''))

#Pauses and resumes download and sends pause message to the queue.
Expand Down Expand Up @@ -892,14 +909,19 @@ def try_set(widget, val):

if path.exists(download_path) == False:
create_directories(download_path)

try_configure(dl_button, text='Pause', command=lambda: self.toggle_pause(dl_button, q))
try_configure(open_button, text='Cancel', state = 'normal', command=lambda: self.cancel(dl_button, q))
try_configure(status, text_color='green', text='Downloading')

i=0
h=0
last_ui_update = 0
downloaded_bytes = 0
size_mb = f"{round(size / 1024000, 2)}"
cancelled = False
with self._download_lock:
self.total_download_size = self.total_download_size + size

#send a request to the update files URL. Handle threads and download behavior based on the button state.
with self.session.get(url, stream=True, verify=False) as r:
Expand All @@ -917,32 +939,26 @@ def try_set(widget, val):
try_configure(status, text_color='green', text='Downloading')
elif action == ButtonAction.STOP:
try_configure(status, text_color = 'red', text='Download Cancelled!')
cancelled = True
break
except queue.Empty:
pass

#Check if download gets cancelled
if status.cget('text') == 'Download Cancelled!':
cancelled = True
break

#Download the file and update the progress bar and status label based on how much has downloaded.
if chunk:
f.write(chunk)
i = i + (1/(size/(len(chunk))))
h = h + len(chunk) / 1024000
downloaded_bytes = downloaded_bytes + len(chunk)
now = time.monotonic()
with self._download_lock:
self.completed_download_size = self.completed_download_size + len(chunk)

now = time.monotonic()

if now - last_ui_update > 0.01:
last_ui_update = now
self.after(0, lambda v = i: try_set(prog_bar, v))
try_configure(status, text_color = 'green', text= f"{h:.2f}/{size_mb}MB")

if q.empty() == False: break

#After the download loop, check if the download was cancelled. If it was, remove the file and update the completed download size.
if cancelled:
try:
Expand All @@ -951,11 +967,14 @@ def try_set(widget, val):
pass
with self._download_lock:
self.completed_download_size = self.completed_download_size - downloaded_bytes
self.total_download_size = self.total_download_size - size


#After the file is downloaded, reconfigure the dl and open button behavior.
#Then remove the file if the dl was cancelled. If it completed, run is_shit_there to check the hash and configure buttons properly.
try_configure(dl_button, command=lambda: App.frame_button_download(self, name, title_id, url, console, size, sha1, index, download_path, fileloc))
try_configure(open_button, text='Open', state = 'disabled', command=lambda: None)

if status.cget('text') == 'Download Cancelled!':
os.remove(fileloc)
else:
Expand Down Expand Up @@ -1012,8 +1031,6 @@ def frame_button_download(self, game_name, title_id, url, console, update_size,
open_button = self.textbox.open_button_list[index]
q = self.textbox.queue_list[index]

with self._download_lock:
self.total_download_size = self.total_download_size + update_size
threading.Thread(target = self.download_updates, args=(url, download_path, update_size, sha1, index, title_id, game_name, console, fileloc, self.download_semaphore, prog_bar, status, dl_button, open_button, q), daemon = True).start()

#Behavior for the Download All button. Opens the download all window.
Expand Down
Loading