Advertisement
Dunkler2Engel

LaunchBox Media Manager

Feb 21st, 2025
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 55.08 KB | None | 0 0
  1. import os
  2. import shutil
  3. import xml.etree.ElementTree as ET
  4. import tkinter as tk
  5. from tkinter import messagebox, filedialog
  6. from PIL import Image, ImageTk
  7. import subprocess
  8. from tkinter import ttk
  9.  
  10.  
  11. class LaunchBoxManager:
  12. def __init__(self, root):
  13. self.root = root
  14. self.root.title("LaunchBox Media Manager")
  15. self.root.geometry("800x600")
  16. self.platforms = {}
  17. self.launchbox_path = None
  18. self.filters = ["Box - Front", "Clear Logo"]
  19. self.show_images = False
  20. self.image_references = {}
  21. self.last_sorted_column = None
  22. self.videos_enabled = False
  23. self.manual_enabled = False
  24. self.sort_ascending = True
  25. self.cell_size_text = (100, 50)
  26. self.cell_size_image = (150, 150)
  27. self.columns = 0
  28. self.image_cache_ratio = "150x150"
  29. self.alternative_path = None
  30. self.max_games_per_page_imagemode = 100
  31. self.make_cache = True
  32. self.current_page = 0
  33. self.total_pages = 0
  34.  
  35. max_retries = 2
  36. retries = 0
  37.  
  38. while retries < max_retries:
  39. if self.load_launchbox_path():
  40. break
  41. else:
  42. print("Error: Could not load LaunchBox configuration. Retrying...")
  43. retries += 1
  44.  
  45. if retries == max_retries:
  46. print("Error: Could not load LaunchBox configuration after several attempts.")
  47. self.root.destroy()
  48.  
  49. self.setup_ui()
  50.  
  51. def setup_ui(self):
  52. # Top frame (for platform selection)
  53. self.top_frame = tk.Frame(self.root)
  54. self.top_frame.pack(fill="x", padx=10, pady=10)
  55.  
  56. # Label and Combobox for platform selection
  57. self.platform_label = tk.Label(self.top_frame, text="Platform:")
  58. self.platform_label.pack(side="left", padx=(0, 5))
  59.  
  60. self.platform_combobox = tk.StringVar()
  61. self.platform_menu = ttk.Combobox(self.top_frame, textvariable=self.platform_combobox, state="readonly")
  62. self.platform_menu.pack(side="left", fill="x", expand=True, padx=(0, 10))
  63. self.platform_combobox.trace("w", self.on_platform_select)
  64.  
  65. # Button to load platforms
  66. self.load_button = tk.Button(self.top_frame, text="Load Platforms", command=self.load_platforms)
  67. self.load_button.pack(side="left")
  68.  
  69. # Frame for Checkbuttons (Image Mode and Hide All YES)
  70. self.checkbutton_frame = tk.Frame(self.top_frame)
  71. self.checkbutton_frame.pack(side="left", padx=(10, 0))
  72.  
  73. # Checkbutton for image mode
  74. self.switch_var = tk.BooleanVar()
  75. self.switch = tk.Checkbutton(self.checkbutton_frame, text="Image Mode", variable=self.switch_var,
  76. command=self.toggle_view_mode)
  77. self.switch.pack(anchor="w") # Aligned to the left within the Frame
  78.  
  79. # Checkbutton to hide games with "YES" in all cells
  80. self.hide_all_si_var = tk.BooleanVar(value=False) # Disabled by default
  81. self.hide_all_si_check = tk.Checkbutton(self.checkbutton_frame, text="Hide All YES",
  82. variable=self.hide_all_si_var,
  83. command=self.toggle_hide_all_si)
  84. self.hide_all_si_check.pack(anchor="w") # Aligned to the left within the Frame
  85.  
  86. # Button to open settings (on the right)
  87. self.settings_button = tk.Button(self.top_frame, text="Settings", command=self.open_settings)
  88. self.settings_button.pack(side="right", padx=(10, 0))
  89.  
  90. # Frame for filters and the table
  91. self.filter_frame = tk.LabelFrame(self.root, text="Image Status")
  92. self.filter_frame.pack(fill="both", expand=True, padx=10, pady=10)
  93.  
  94. # Canvas and scrollbars for the table
  95. self.header_canvas = tk.Canvas(self.filter_frame, borderwidth=1, relief="solid", height=self.cell_size_text[1])
  96. self.header_canvas.pack(fill="x", side="top")
  97.  
  98. self.header_scrollbar_x = tk.Scrollbar(self.filter_frame, orient="horizontal",
  99. command=lambda *args: self.sync_scroll(*args))
  100. self.header_scrollbar_x.pack(side="top", fill="x")
  101. self.header_canvas.config(xscrollcommand=self.header_scrollbar_x.set)
  102.  
  103. self.canvas_frame = tk.Frame(self.filter_frame)
  104. self.canvas_frame.pack(fill="both", expand=True)
  105.  
  106. self.canvas = tk.Canvas(self.canvas_frame, borderwidth=1, relief="solid")
  107. self.canvas.pack(side="left", fill="both", expand=True)
  108.  
  109. self.canvas_scrollbar_y = tk.Scrollbar(self.canvas_frame, orient="vertical", command=self.canvas.yview)
  110. self.canvas_scrollbar_y.pack(side="right", fill="y")
  111. self.canvas.config(yscrollcommand=self.canvas_scrollbar_y.set)
  112.  
  113. self.canvas_scrollbar_x = tk.Scrollbar(self.filter_frame, orient="horizontal",
  114. command=lambda *args: self.sync_scroll(*args))
  115. self.canvas_scrollbar_x.pack(side="bottom", fill="x")
  116. self.canvas.config(xscrollcommand=self.canvas_scrollbar_x.set)
  117.  
  118. self.canvas.bind("<Configure>", lambda e: self.sync_scroll())
  119.  
  120. # Bind mouse wheel event to the Canvas
  121. self.canvas.bind("<MouseWheel>", self.on_mouse_wheel_cells)
  122.  
  123. # Frame for pagination and additional buttons
  124. self.pagination_frame = tk.Frame(self.root)
  125. self.pagination_frame.pack(fill="x", padx=10, pady=10)
  126.  
  127. self.center_frame = tk.Frame(self.pagination_frame)
  128. self.center_frame.pack()
  129.  
  130. # "Previous" button
  131. self.prev_button = tk.Button(self.center_frame, text="Previous", command=self.prev_page)
  132. self.prev_button.pack(side="left")
  133.  
  134. # Current page label
  135. self.page_label = tk.Label(self.center_frame, text="Page 1 of 1")
  136. self.page_label.pack(side="left", padx=10)
  137.  
  138. # "Next" button
  139. self.next_button = tk.Button(self.center_frame, text="Next", command=self.next_page)
  140. self.next_button.pack(side="left")
  141.  
  142. # "Generate All Cache" button
  143. self.generate_all_cache_button = tk.Button(self.center_frame, text="Generate All Cache",
  144. command=self.generate_all_cache)
  145. self.generate_all_cache_button.pack(side="left", padx=(10, 0))
  146.  
  147. # "Regenerate Cache" button
  148. self.regenerate_cache_button = tk.Button(self.center_frame, text="Regenerate Cache",
  149. command=self.regenerate_cache)
  150. self.regenerate_cache_button.pack(side="left", padx=(10, 0))
  151.  
  152. # Hide or show cache buttons based on make_cache
  153. self.update_cache_buttons_visibility()
  154.  
  155. def on_mouse_wheel_cells(self, event):
  156. menu_state = str(self.platform_menu.cget("state")) # Save it as an immutable string
  157.  
  158. if menu_state == "readonly":
  159. delta = -event.delta
  160. if delta > 0:
  161. self.canvas.yview_scroll(1, "units")
  162. else:
  163. self.canvas.yview_scroll(-1, "units")
  164.  
  165. def on_mouse_wheel_platforms(self, event):
  166. """
  167. Handles vertical scrolling with the mouse wheel for the platforms list.
  168. """
  169. menu_state = self.platform_menu.cget("state")
  170.  
  171. if menu_state == "normal": # Only scroll if the menu is expanded
  172. current_index = self.platform_menu.current()
  173. if event.delta > 0:
  174. new_index = max(0, current_index - 1) # Scroll up
  175. else:
  176. new_index = min(len(self.platform_menu["values"]) - 1, current_index + 1) # Scroll down
  177. self.platform_menu.current(new_index)
  178. self.platform_combobox.set(self.platform_menu["values"][new_index])
  179. self.on_platform_select()
  180.  
  181. def sync_scroll(self, *args):
  182. """
  183. Synchronizes horizontal scrolling between the header_canvas and the main canvas.
  184. Does not affect vertical scrolling.
  185. """
  186. if len(args) == 2 and args[0] == "moveto":
  187. fraction = float(args[1])
  188. self.header_canvas.xview_moveto(fraction)
  189. self.canvas.xview_moveto(fraction)
  190. elif len(args) == 2 and args[0] == "scroll":
  191. amount, units = args[1].split()
  192. self.header_canvas.xview_scroll(amount, units)
  193. self.canvas.xview_scroll(amount, units)
  194. else:
  195. fraction = self.canvas.xview()[0]
  196. self.header_canvas.xview_moveto(fraction)
  197.  
  198. def toggle_hide_all_si(self):
  199. # If the Checkbutton is enabled, hide games with "YES" in all cells
  200. if self.hide_all_si_var.get():
  201. # Sort games alphabetically before applying the filter
  202. selected_platform = self.platform_combobox.get()
  203. if not selected_platform:
  204. return
  205.  
  206. xml_path = self.platforms[selected_platform]
  207. tree = ET.parse(xml_path)
  208. root = tree.getroot()
  209.  
  210. games = root.findall("Game")
  211. game_dict = {}
  212.  
  213. for game in games:
  214. title_element = game.find("Title")
  215. title = title_element.text if title_element is not None else None
  216.  
  217. app_path_element = game.find("ApplicationPath")
  218. if app_path_element is not None and app_path_element.text:
  219. app_filename = os.path.basename(app_path_element.text)
  220. app_name = os.path.splitext(app_filename)[0]
  221. else:
  222. app_name = None
  223.  
  224. if title:
  225. game_dict[title] = [title]
  226. if app_name and app_name != title:
  227. game_dict[title].append(app_name)
  228.  
  229. # Sort games alphabetically (from A to Z)
  230. sorted_game_dict = dict(
  231. sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
  232.  
  233. # Apply the "Hide All YES" filter to the sorted list
  234. self.hide_all_si(sorted_game_dict)
  235. else:
  236. # If disabled, show all games again
  237. self.on_platform_select()
  238.  
  239. def update_cache_buttons_visibility(self):
  240. """Updates the visibility of cache buttons based on the value of make_cache."""
  241. if self.make_cache:
  242. self.generate_all_cache_button.pack(side="left", padx=(10, 0))
  243. self.regenerate_cache_button.pack(side="left", padx=(10, 0))
  244. else:
  245. self.generate_all_cache_button.pack_forget()
  246. self.regenerate_cache_button.pack_forget()
  247.  
  248. def load_platforms(self):
  249. self.platform_combobox.set("") # Clear current selection
  250. self.platforms.clear() # Clear the platforms dictionary
  251.  
  252. # Build the path to the platforms folder
  253. platforms_path = os.path.join(self.launchbox_path, "Data", "Platforms").replace("\\", "/")
  254. if not os.path.exists(platforms_path):
  255. messagebox.showerror("Error", f"Platforms folder not found at: {platforms_path}")
  256. return
  257.  
  258. # Get platform names
  259. platform_names = []
  260. for filename in os.listdir(platforms_path):
  261. if filename.endswith(".xml"):
  262. platform_name = filename[:-4] # Remove the .xml extension
  263. self.platforms[platform_name] = os.path.join(platforms_path, filename).replace("\\", "/")
  264. platform_names.append(platform_name)
  265.  
  266. if not platform_names:
  267. messagebox.showerror("Error", "No XML files found in the platforms folder.")
  268. return
  269.  
  270. # Assign values to the Combobox
  271. self.platform_menu["values"] = platform_names
  272. self.platform_menu.current(0) # Select the first platform by default
  273.  
  274. # Force UI update
  275. self.platform_menu.update_idletasks()
  276.  
  277. # Load data for the selected platform
  278. self.on_platform_select()
  279.  
  280. def hide_all_si(self, game_dict=None):
  281. if game_dict is None:
  282. selected_platform = self.platform_combobox.get()
  283. if not selected_platform:
  284. return
  285.  
  286. xml_path = self.platforms[selected_platform]
  287. tree = ET.parse(xml_path)
  288. root = tree.getroot()
  289.  
  290. games = root.findall("Game")
  291. game_dict = {}
  292.  
  293. for game in games:
  294. title_element = game.find("Title")
  295. title = title_element.text if title_element is not None else None
  296.  
  297. app_path_element = game.find("ApplicationPath")
  298. if app_path_element is not None and app_path_element.text:
  299. app_filename = os.path.basename(app_path_element.text)
  300. app_name = os.path.splitext(app_filename)[0]
  301. else:
  302. app_name = None
  303.  
  304. if title:
  305. game_dict[title] = [title]
  306. if app_name and app_name != title:
  307. game_dict[title].append(app_name)
  308.  
  309. # Filter games that have "YES" in all cells
  310. filtered_game_dict = self.filter_all_si(game_dict)
  311.  
  312. # Update the table with the filtered games
  313. selected_platform = self.platform_combobox.get()
  314. self.load_filter_file(selected_platform, filtered_game_dict)
  315.  
  316. def on_platform_select(self, *args):
  317. selected_platform = self.platform_combobox.get()
  318. if not selected_platform:
  319. return
  320.  
  321. try:
  322. xml_path = self.platforms[selected_platform]
  323. tree = ET.parse(xml_path)
  324. root = tree.getroot()
  325.  
  326. if root.tag != "LaunchBox":
  327. messagebox.showerror("Error", f"The XML file does not have the expected structure. Root element: {root.tag}")
  328. return
  329.  
  330. games = root.findall("Game")
  331. game_dict = {}
  332.  
  333. for game in games:
  334. title_element = game.find("Title")
  335. title = title_element.text if title_element is not None else None
  336.  
  337. app_path_element = game.find("ApplicationPath")
  338. if app_path_element is not None and app_path_element.text:
  339. app_filename = os.path.basename(app_path_element.text)
  340. app_name = os.path.splitext(app_filename)[0]
  341. else:
  342. app_name = None
  343.  
  344. if title:
  345. game_dict[title] = [title]
  346. if app_name and app_name != title:
  347. game_dict[title].append(app_name)
  348.  
  349. sorted_game_dict = dict(sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
  350. self.load_filter_file(selected_platform, sorted_game_dict)
  351. except Exception as e:
  352. messagebox.showerror("Error", f"Could not read the XML file: {e}")
  353.  
  354. def generate_all_cache(self):
  355. selected_platform = self.platform_combobox.get()
  356. if not selected_platform:
  357. return
  358.  
  359. self.progress_label = tk.Label(self.root, text="Generating Cache... 0%")
  360. self.progress_label.place(relx=0.5, rely=0.5, anchor="center")
  361. self.root.update()
  362.  
  363. original_page = self.current_page
  364.  
  365. for page in range(self.total_pages):
  366. self.current_page = page
  367. self.on_platform_select()
  368. self.progress_label.config(text=f"Generating page {page + 1} of {self.total_pages}")
  369. self.root.update()
  370.  
  371. self.current_page = original_page
  372. self.on_platform_select()
  373. self.progress_label.destroy()
  374.  
  375. def open_settings(self):
  376. current_dir = os.path.dirname(os.path.abspath(__file__))
  377. settings_path = os.path.join(current_dir, "Settings.py")
  378. subprocess.run(["python", settings_path], check=True)
  379.  
  380. # Reload configuration after saving changes
  381. self.load_launchbox_path()
  382.  
  383. # Update cache buttons visibility
  384. self.update_cache_buttons_visibility()
  385.  
  386. # Update other UI elements if necessary
  387. self.on_platform_select() # This updates the table with the new values
  388.  
  389. def load_launchbox_path(self):
  390. config_file = os.path.join(os.path.dirname(__file__), "config.txt").replace("\\", "/")
  391.  
  392. if not os.path.exists(config_file):
  393. with open(config_file, "w") as file:
  394. file.write("path=\n")
  395. file.write("filters=\n")
  396. file.write("image_cache_ratio=150x150\n")
  397. file.write("alternative_path=\n")
  398. file.write("image_cell=150x150\n")
  399. file.write("text_cell=150x50\n")
  400. file.write("max_games_per_page_imagemode=100\n")
  401. file.write("color_media_yes_title=#0073E6\n")
  402. file.write("color_media_yes_rom=#00E673\n")
  403. file.write("color_media_both=#E67300\n")
  404. file.write("color_media_no=#E60073\n")
  405. file.write("color_no_trans=#C0C0C0\n")
  406. file.write("make_cache=true\n")
  407. print("config.txt file created with the basic structure.")
  408. return False
  409.  
  410. with open(config_file, "r") as file:
  411. config_lines = file.readlines()
  412. for line in config_lines:
  413. if line.startswith("path="):
  414. self.launchbox_path = line.strip().split('=')[1]
  415. if line.startswith("filters="):
  416. filters_line = line.strip().split('=')[1]
  417. self.filters = [f.strip().strip('"') for f in filters_line.split(',')]
  418. if line.startswith("image_cache_ratio="):
  419. self.image_cache_ratio = line.strip().split('=')[1]
  420. if line.startswith("alternative_path="):
  421. self.alternative_path = line.strip().split('=')[1]
  422. if line.startswith("image_cell="):
  423. try:
  424. width, height = line.strip().split('=')[1].split('x')
  425. self.cell_size_image = (int(width), int(height))
  426. except ValueError:
  427. print(
  428. "Error: The value of image_cell is not in the correct format. Using default values (200x200).")
  429. self.cell_size_image = (200, 200)
  430. if line.startswith("text_cell="):
  431. try:
  432. width, height = line.strip().split('=')[1].split('x')
  433. self.cell_size_text = (int(width), int(height))
  434. except ValueError:
  435. print(
  436. "Error: The value of text_cell is not in the correct format. Using default values (100x50).")
  437. self.cell_size_text = (100, 50)
  438. if line.startswith("max_games_per_page_imagemode="):
  439. try:
  440. self.max_games_per_page_imagemode = int(line.strip().split('=')[1])
  441. except ValueError:
  442. print(
  443. "Error: The value of max_games_per_page_imagemode is not a valid number. Using default value (20).")
  444. self.max_games_per_page_imagemode = 20
  445. if line.startswith("color_media_yes_title="):
  446. self.color_media_yes_title = line.strip().split('=')[1]
  447. if line.startswith("color_media_yes_rom="):
  448. self.color_media_yes_rom = line.strip().split('=')[1]
  449. if line.startswith("color_media_both="):
  450. self.color_media_both = line.strip().split('=')[1]
  451. if line.startswith("color_media_no="):
  452. self.color_media_no = line.strip().split('=')[1]
  453. if line.startswith("color_no_trans="):
  454. self.color_no_trans = line.strip().split('=')[1]
  455. if line.startswith("color_media_both="):
  456. self.color_media_both = line.strip().split('=')[1]
  457. if line.startswith("make_cache="): # New parameter
  458. self.make_cache = line.strip().split('=')[1].lower() == "true"
  459.  
  460. return True
  461.  
  462. def load_platforms(self):
  463. self.platform_combobox.set("") # Clear current selection
  464. self.platforms.clear() # Clear the platforms dictionary
  465.  
  466. # Build the path to the platforms folder
  467. platforms_path = os.path.join(self.launchbox_path, "Data", "Platforms").replace("\\", "/")
  468. if not os.path.exists(platforms_path):
  469. messagebox.showerror("Error", f"Platforms folder not found at: {platforms_path}")
  470. return
  471.  
  472. # Get platform names
  473. platform_names = []
  474. for filename in os.listdir(platforms_path):
  475. if filename.endswith(".xml"):
  476. platform_name = filename[:-4] # Remove the .xml extension
  477. self.platforms[platform_name] = os.path.join(platforms_path, filename).replace("\\", "/")
  478. platform_names.append(platform_name)
  479.  
  480.  
  481. if not platform_names:
  482. messagebox.showerror("Error", "No XML files found in the platforms folder.")
  483. return
  484.  
  485. # Assign values to the Combobox
  486. self.platform_menu["values"] = platform_names
  487. self.platform_menu.current(0) # Select the first platform by default
  488.  
  489. # Force UI update
  490. self.platform_menu.update_idletasks()
  491.  
  492. # Load data for the selected platform
  493. self.on_platform_select()
  494.  
  495. def on_platform_select(self, *args):
  496. selected_platform = self.platform_combobox.get()
  497. if not selected_platform:
  498. return
  499.  
  500. try:
  501. xml_path = self.platforms[selected_platform]
  502. tree = ET.parse(xml_path)
  503. root = tree.getroot()
  504.  
  505. if root.tag != "LaunchBox":
  506. messagebox.showerror("Error", f"The XML file does not have the expected structure. Root element: {root.tag}")
  507. return
  508.  
  509. games = root.findall("Game")
  510. game_dict = {}
  511.  
  512. for game in games:
  513. title_element = game.find("Title")
  514. title = title_element.text if title_element is not None else None
  515.  
  516. app_path_element = game.find("ApplicationPath")
  517. if app_path_element is not None and app_path_element.text:
  518. app_filename = os.path.basename(app_path_element.text)
  519. app_name = os.path.splitext(app_filename)[0]
  520. else:
  521. app_name = None
  522.  
  523. if title:
  524. game_dict[title] = [title]
  525. if app_name and app_name != title:
  526. game_dict[title].append(app_name)
  527.  
  528. sorted_game_dict = dict(sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
  529. self.load_filter_file(selected_platform, sorted_game_dict)
  530. except Exception as e:
  531. messagebox.showerror("Error", f"Could not read the XML file: {e}")
  532.  
  533. def load_filter_file(self, platform, game_dict):
  534. filters_to_use = self.filters[:]
  535. if self.videos_enabled:
  536. filters_to_use.append("Videos")
  537. if self.manual_enabled:
  538. filters_to_use.append("Manual")
  539.  
  540. self.columns = len(filters_to_use) + 1 # Define self.columns
  541. self.total_pages = (len(game_dict) + self.max_games_per_page_imagemode - 1) // self.max_games_per_page_imagemode
  542. self.update_pagination_controls()
  543.  
  544. start_index = self.current_page * self.max_games_per_page_imagemode
  545. end_index = start_index + self.max_games_per_page_imagemode
  546. games_to_show = list(game_dict.items())[start_index:end_index]
  547.  
  548. self.canvas.delete("all")
  549. self.image_references = {}
  550. self.draw_table(games_to_show, filters_to_use)
  551.  
  552. total_width = sum(
  553. self.cell_size_image[0] if self.show_images else self.cell_size_text[0] for _ in range(self.columns))
  554. total_height = self.rows * (self.cell_size_image[1] if self.show_images else self.cell_size_text[1])
  555. self.canvas.config(scrollregion=(0, 0, total_width, total_height))
  556.  
  557. def prev_page(self):
  558. if self.current_page > 0:
  559. self.current_page -= 1
  560. self.update_pagination_controls()
  561. self.on_platform_select()
  562.  
  563. def next_page(self):
  564. if self.current_page < self.total_pages - 1:
  565. self.current_page += 1
  566. self.update_pagination_controls()
  567. self.on_platform_select()
  568.  
  569. def update_pagination_controls(self):
  570. self.page_label.config(text=f"Page {self.current_page + 1} of {self.total_pages}")
  571. self.prev_button.config(state="normal" if self.current_page > 0 else "disabled")
  572. self.next_button.config(state="normal" if self.current_page < self.total_pages - 1 else "disabled")
  573.  
  574. def toggle_view_mode(self):
  575. self.show_images = self.switch_var.get()
  576.  
  577. if self.show_images:
  578. self.header_canvas.config(height=self.cell_size_image[1])
  579. else:
  580. self.header_canvas.config(height=self.cell_size_text[1])
  581.  
  582. self.on_platform_select()
  583.  
  584. def search_image_in_folder(self, folder, search_names, extensions):
  585. """
  586. Searches for images in a folder and its subfolders recursively.
  587. Returns the image path if found, or None if not found.
  588. """
  589. for root, dirs, files in os.walk(folder):
  590. for file in files:
  591. # Check if the file matches any of the names and extensions
  592. for search_name in search_names:
  593. for ext in extensions:
  594. if file.startswith(search_name) and file.endswith(ext):
  595. return os.path.join(root, file), search_name
  596. return None, None
  597.  
  598. def draw_table(self, games_to_show, filters):
  599. self.rows = len(games_to_show)
  600.  
  601. if self.show_images:
  602. cell_width, cell_height = self.cell_size_image
  603. else:
  604. cell_width, cell_height = self.cell_size_text
  605.  
  606. total_width = sum(cell_width for _ in range(self.columns))
  607.  
  608. self.header_canvas.config(width=total_width, height=cell_height)
  609. self.header_canvas.delete("all")
  610. for col, filter_name in enumerate(["Title"] + filters):
  611. self.draw_cell(0, col, filter_name, header=True, canvas=self.header_canvas)
  612. self.header_canvas.tag_bind(f"header_{col}", "<Button-1>", lambda event, col=col: self.on_header_click(col))
  613.  
  614. for row, (title, search_names) in enumerate(games_to_show, start=0):
  615. self.draw_cell(row, 0, title)
  616. for col, filter_name in enumerate(filters, start=1):
  617. if filter_name == "Videos":
  618. # Define main and alternative paths
  619. main_folder_path = os.path.join(self.launchbox_path, "Videos",
  620. self.platform_combobox.get()).replace("\\", "/")
  621. alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
  622. "Videos").replace("\\",
  623. "/") if self.alternative_path else None
  624.  
  625. file_found = False
  626. match_type = None
  627. file_path = None
  628.  
  629. # Search in the alternative path first (if it exists)
  630. if alternative_folder_path and os.path.exists(alternative_folder_path):
  631. # Search for <Title>-01.mp4
  632. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.mp4").replace("\\",
  633. "/")
  634. if os.path.isfile(file_path):
  635. file_found = True
  636. match_type = 'title'
  637. else:
  638. # Search for <ApplicationPath>.mp4 (without -01)
  639. if len(search_names) > 1:
  640. file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.mp4").replace(
  641. "\\", "/")
  642. if os.path.isfile(file_path):
  643. file_found = True
  644. match_type = 'rom'
  645. else:
  646. # If <Title> and <ApplicationPath> are the same, search for <Title>.mp4 (without -01)
  647. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}.mp4").replace(
  648. "\\", "/")
  649. if os.path.isfile(file_path):
  650. file_found = True
  651. match_type = 'rom'
  652.  
  653. # If not found in the alternative path, search in the main path
  654. if not file_found and os.path.exists(main_folder_path):
  655. # Search for <Title>-01.mp4
  656. file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.mp4").replace("\\", "/")
  657. if os.path.isfile(file_path):
  658. file_found = True
  659. match_type = 'title'
  660. else:
  661. # Search for <ApplicationPath>.mp4 (without -01)
  662. if len(search_names) > 1:
  663. file_path = os.path.join(main_folder_path, f"{search_names[1]}.mp4").replace("\\", "/")
  664. if os.path.isfile(file_path):
  665. file_found = True
  666. match_type = 'rom'
  667. else:
  668. # If <Title> and <ApplicationPath> are the same, search for <Title>.mp4 (without -01)
  669. file_path = os.path.join(main_folder_path, f"{search_names[0]}.mp4").replace("\\", "/")
  670. if os.path.isfile(file_path):
  671. file_found = True
  672. match_type = 'rom'
  673.  
  674. # Show the result in the table
  675. if self.show_images:
  676. if file_found:
  677. self.draw_cell(row, col, "YES",
  678. cell_color=self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom)
  679. else:
  680. self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
  681. else:
  682. cell_value = "YES (Title)" if match_type == 'title' else "YES (Rom)" if match_type == 'rom' else "NO"
  683. cell_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom if match_type == 'rom' else self.color_media_no
  684. self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
  685.  
  686. elif filter_name == "Manual":
  687. # Define main and alternative paths
  688. main_folder_path = os.path.join(self.launchbox_path, "Manuals",
  689. self.platform_combobox.get()).replace("\\", "/")
  690. alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
  691. "Manuals").replace("\\",
  692. "/") if self.alternative_path else None
  693.  
  694. file_found = False
  695. match_type = None
  696. file_path = None
  697.  
  698. # Search in the alternative path first (if it exists)
  699. if alternative_folder_path and os.path.exists(alternative_folder_path):
  700. # Search for <Title>-01.pdf
  701. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.pdf").replace("\\",
  702. "/")
  703. if os.path.isfile(file_path):
  704. file_found = True
  705. match_type = 'title'
  706. else:
  707. # Search for <ApplicationPath>.pdf (without -01)
  708. if len(search_names) > 1:
  709. file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.pdf").replace(
  710. "\\", "/")
  711. if os.path.isfile(file_path):
  712. file_found = True
  713. match_type = 'rom'
  714. else:
  715. # If <Title> and <ApplicationPath> are the same, search for <Title>.pdf (without -01)
  716. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}.pdf").replace(
  717. "\\", "/")
  718. if os.path.isfile(file_path):
  719. file_found = True
  720. match_type = 'rom'
  721.  
  722. # If not found in the alternative path, search in the main path
  723. if not file_found and os.path.exists(main_folder_path):
  724. # Search for <Title>-01.pdf
  725. file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.pdf").replace("\\", "/")
  726. if os.path.isfile(file_path):
  727. file_found = True
  728. match_type = 'title'
  729. else:
  730. # Search for <ApplicationPath>.pdf (without -01)
  731. if len(search_names) > 1:
  732. file_path = os.path.join(main_folder_path, f"{search_names[1]}.pdf").replace("\\", "/")
  733. if os.path.isfile(file_path):
  734. file_found = True
  735. match_type = 'rom'
  736. else:
  737. # If <Title> and <ApplicationPath> are the same, search for <Title>.pdf (without -01)
  738. file_path = os.path.join(main_folder_path, f"{search_names[0]}.pdf").replace("\\", "/")
  739. if os.path.isfile(file_path):
  740. file_found = True
  741. match_type = 'rom'
  742.  
  743. # Show the result in the table
  744. if self.show_images:
  745. if file_found:
  746. self.draw_cell(row, col, "YES",
  747. cell_color=self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom)
  748. else:
  749. self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
  750. else:
  751. cell_value = "YES (Title)" if match_type == 'title' else "YES (Rom)" if match_type == 'rom' else "NO"
  752. cell_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom if match_type == 'rom' else self.color_media_no
  753. self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
  754.  
  755. else:
  756. image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(),
  757. filter_name).replace("\\", "/")
  758. image_found = False
  759. image_path = None
  760. match_type = None
  761. title_found = False
  762. rom_found = False
  763. title_path = None
  764. rom_path = None
  765.  
  766. if os.path.exists(image_folder):
  767. # Search for <Title>-01.ext recursively
  768. for ext in ["png", "jpg"]:
  769. for root, dirs, files in os.walk(image_folder):
  770. for file in files:
  771. if file.startswith(f"{search_names[0]}-01") and file.endswith(ext):
  772. title_path = os.path.join(root, file)
  773. title_found = True
  774. break
  775. if title_found:
  776. break
  777. if title_found:
  778. break
  779.  
  780. # Search for <ApplicationPath>.ext non-recursively
  781. if len(search_names) > 1:
  782. for ext in ["png", "jpg"]:
  783. rom_path = os.path.join(image_folder, f"{search_names[1]}.{ext}").replace("\\", "/")
  784. if os.path.isfile(rom_path):
  785. rom_found = True
  786. break
  787. else:
  788. # If <Title> and <ApplicationPath> are the same, search for <Title>.ext (without -01)
  789. for ext in ["png", "jpg"]:
  790. rom_path = os.path.join(image_folder, f"{search_names[0]}.{ext}").replace("\\", "/")
  791. if os.path.isfile(rom_path):
  792. rom_found = True
  793. break
  794.  
  795. # Determine the match type
  796. if title_found and rom_found:
  797. match_type = 'title/rom'
  798. image_path = title_path # Use the Title image to display
  799. elif title_found:
  800. match_type = 'title'
  801. image_path = title_path
  802. elif rom_found:
  803. match_type = 'rom'
  804. image_path = rom_path
  805.  
  806. if self.show_images:
  807. if image_path:
  808. self.draw_image_cell(row, col, image_path, match_type)
  809. else:
  810. self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
  811. else:
  812. if match_type == 'title/rom':
  813. cell_value = "YES (Title/Rom)"
  814. cell_color = self.color_media_both
  815. elif match_type == 'title':
  816. cell_value = "YES (Title)"
  817. cell_color = self.color_media_yes_title
  818. elif match_type == 'rom':
  819. cell_value = "YES (Rom)"
  820. cell_color = self.color_media_yes_rom
  821. else:
  822. cell_value = "NO"
  823. cell_color = self.color_media_no
  824. self.draw_cell(row, col, cell_value, cell_color=cell_color, is_text=True)
  825.  
  826. total_width = sum(
  827. self.cell_size_image[0] if self.show_images else self.cell_size_text[0] for _ in range(self.columns))
  828. total_height = len(games_to_show) * (self.cell_size_image[1] if self.show_images else self.cell_size_text[1])
  829. self.canvas.config(scrollregion=(0, 0, total_width, total_height))
  830. self.header_canvas.config(scrollregion=(0, 0, total_width, cell_height))
  831.  
  832. # Force focus to the Canvas to receive mouse wheel events
  833. self.canvas.focus_set()
  834.  
  835. def on_header_click(self, col):
  836. # Call the method to sort the data
  837. self.sort_by_column(col)
  838.  
  839. def draw_cell(self, row, col, value, header=False, cell_color=None, is_text=False, canvas=None):
  840. if canvas is None:
  841. canvas = self.canvas
  842.  
  843. if header:
  844. if self.show_images:
  845. cell_width, cell_height = self.cell_size_image
  846. else:
  847. cell_width, cell_height = self.cell_size_text
  848. elif is_text or not self.show_images:
  849. cell_width, cell_height = self.cell_size_text
  850. else:
  851. cell_width, cell_height = self.cell_size_image
  852.  
  853. x1 = col * cell_width
  854. y1 = row * cell_height
  855. x2 = x1 + cell_width
  856. y2 = y1 + cell_height
  857.  
  858. if value == "YES (Title)":
  859. fill_color = self.color_media_yes_title
  860. elif value == "YES (Rom)":
  861. fill_color = self.color_media_yes_rom
  862. elif value == "NO":
  863. fill_color = self.color_media_no
  864. else:
  865. fill_color = cell_color or "white"
  866.  
  867. # Draw the cell
  868. canvas.create_rectangle(x1, y1, x2, y2, fill=fill_color, outline="black",
  869. tags=f"header_{col}" if header else f"cell_{row}_{col}")
  870.  
  871. font_style = ("Arial", 10, "bold") if header else ("Arial", 10)
  872. wrapped_text = self.wrap_text(value, cell_width - 10)
  873. text_y = y1 + (cell_height - len(wrapped_text) * 12) / 2
  874.  
  875. for line in wrapped_text:
  876. canvas.create_text(x1 + 5, text_y, anchor="w", text=line, font=font_style,
  877. tags=f"header_{col}" if header else f"cell_{row}_{col}")
  878. text_y += 12
  879.  
  880. def wrap_text(self, text, max_width):
  881. import textwrap
  882. return textwrap.wrap(text, width=max_width // 7)
  883.  
  884. def draw_image_cell(self, row, col, image_path, match_type):
  885. try:
  886. cell_width, cell_height = self.cell_size_image
  887.  
  888. if self.make_cache:
  889. # Load from cache
  890. cache_folder = os.path.join(os.path.dirname(__file__), "cache", self.platform_combobox.get(),
  891. self.filters[col - 1])
  892. os.makedirs(cache_folder, exist_ok=True)
  893.  
  894. cache_image_path = os.path.join(cache_folder,
  895. os.path.basename(image_path).replace(".png", ".jpg").replace(".PNG",
  896. ".jpg"))
  897.  
  898. width, height = map(int, self.image_cache_ratio.split("x"))
  899.  
  900. if not os.path.exists(cache_image_path):
  901. try:
  902. file_size = os.path.getsize(image_path)
  903. if file_size > 100 * 1024 * 1024:
  904. raise ValueError(
  905. f"The file {image_path} is too large ({file_size / (1024 * 1024):.2f} MB)")
  906.  
  907. with Image.open(image_path) as image:
  908. if image.mode in ("P", "1", "L", "LA"):
  909. image = image.convert("RGBA")
  910.  
  911. if image.mode == "RGBA":
  912. background = Image.new("RGB", image.size, self.color_no_trans)
  913. background.paste(image, mask=image.split()[-1])
  914. image = background
  915.  
  916. image.thumbnail((width, height))
  917. image.save(cache_image_path, "JPEG")
  918.  
  919. except Exception as e:
  920. print(f"Error processing image {image_path}: {e}")
  921. with open("images_with_errors.txt", "a") as error_file:
  922. error_file.write(f"{image_path}\n")
  923. self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
  924. return
  925.  
  926. with Image.open(cache_image_path) as image:
  927. tk_image = ImageTk.PhotoImage(image)
  928. self.image_references[(row, col)] = tk_image
  929.  
  930. else:
  931. # Load directly from the source
  932. with Image.open(image_path) as image:
  933. if image.mode in ("P", "1", "L", "LA"):
  934. image = image.convert("RGBA")
  935.  
  936. if image.mode == "RGBA":
  937. background = Image.new("RGB", image.size, self.color_no_trans)
  938. background.paste(image, mask=image.split()[-1])
  939. image = background
  940.  
  941. image.thumbnail((cell_width, cell_height))
  942. tk_image = ImageTk.PhotoImage(image)
  943. self.image_references[(row, col)] = tk_image
  944.  
  945. x1 = col * self.cell_size_image[0]
  946. y1 = row * self.cell_size_image[1]
  947.  
  948. bg_color = self.color_media_yes_title if match_type == 'title' else self.color_media_yes_rom
  949. self.canvas.create_rectangle(x1, y1, x1 + cell_width, y1 + cell_height, fill=bg_color, outline="black")
  950. self.canvas.create_image(x1 + cell_width // 2, y1 + cell_height // 2, anchor="center", image=tk_image)
  951. except Exception as e:
  952. print(f"Unexpected error processing image {image_path}: {e}")
  953. self.draw_cell(row, col, "NO", cell_color=self.color_media_no)
  954. with open("images_with_errors.txt", "a") as error_file:
  955. error_file.write(f"{image_path}\n")
  956.  
  957. def regenerate_cache(self):
  958. selected_platform = self.platform_combobox.get()
  959. if not selected_platform:
  960. return
  961.  
  962. self.progress_label = tk.Label(self.root, text="Generating Cache... 0%")
  963. self.progress_label.place(relx=0.5, rely=0.5, anchor="center")
  964. self.root.update()
  965.  
  966. current_platform = self.platform_combobox.get()
  967. cache_folder = os.path.join(os.path.dirname(__file__), "cache", current_platform)
  968. if os.path.exists(cache_folder):
  969. shutil.rmtree(cache_folder)
  970.  
  971. self.load_platforms()
  972. self.platform_combobox.set(current_platform)
  973. self.on_platform_select()
  974. self.progress_label.destroy()
  975.  
  976. def natural_sort_key(self, text):
  977. import re
  978. return [int(text) if text.isdigit() else text.lower() for text in re.split(r'(\d+)', text)]
  979.  
  980. def sort_by_column(self, col):
  981. selected_platform = self.platform_combobox.get()
  982. if not selected_platform:
  983. return
  984.  
  985. xml_path = self.platforms[selected_platform]
  986. tree = ET.parse(xml_path)
  987. root = tree.getroot()
  988.  
  989. games = root.findall("Game")
  990. game_dict = {}
  991.  
  992. for game in games:
  993. title_element = game.find("Title")
  994. title = title_element.text if title_element is not None else None
  995.  
  996. app_path_element = game.find("ApplicationPath")
  997. if app_path_element is not None and app_path_element.text:
  998. app_filename = os.path.basename(app_path_element.text)
  999. app_name = os.path.splitext(app_filename)[0]
  1000. else:
  1001. app_name = None
  1002.  
  1003. if title:
  1004. game_dict[title] = [title]
  1005. if app_name and app_name != title:
  1006. game_dict[title].append(app_name)
  1007.  
  1008. # Sort games
  1009. if col == 0:
  1010. # Sort by title
  1011. sorted_game_dict = dict(
  1012. sorted(game_dict.items(), key=lambda item: (self.natural_sort_key(item[0]), item[0])))
  1013. else:
  1014. # Sort by the selected column
  1015. filter_name = self.filters[col - 1]
  1016. sorted_game_dict = dict(
  1017. sorted(game_dict.items(), key=lambda item: self.get_filter_value(item[1], filter_name)))
  1018.  
  1019. # Toggle between ascending and descending order
  1020. if self.last_sorted_column == col:
  1021. self.sort_ascending = not self.sort_ascending
  1022. if not self.sort_ascending:
  1023. sorted_game_dict = dict(reversed(list(sorted_game_dict.items())))
  1024. else:
  1025. self.sort_ascending = True
  1026.  
  1027. self.last_sorted_column = col
  1028.  
  1029. # If the "Hide All YES" filter is enabled, apply the filter after sorting
  1030. if self.hide_all_si_var.get():
  1031. sorted_game_dict = self.filter_all_si(sorted_game_dict)
  1032.  
  1033. # Update the GUI with the sorted (and filtered if necessary) games
  1034. self.load_filter_file(selected_platform, sorted_game_dict)
  1035.  
  1036. def filter_all_si(self, game_dict):
  1037. """
  1038. Filters games that have "YES" in all cells.
  1039. """
  1040. filtered_game_dict = {}
  1041. for title, search_names in game_dict.items():
  1042. all_si = True
  1043. for filter_name in self.filters:
  1044. if filter_name == "Videos":
  1045. # Logic for videos
  1046. main_folder_path = os.path.join(self.launchbox_path, "Videos",
  1047. self.platform_combobox.get()).replace("\\", "/")
  1048. alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
  1049. "Videos").replace("\\",
  1050. "/") if self.alternative_path else None
  1051.  
  1052. file_found = False
  1053. if alternative_folder_path and os.path.exists(alternative_folder_path):
  1054. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.mp4").replace("\\",
  1055. "/")
  1056. if os.path.isfile(file_path):
  1057. file_found = True
  1058. else:
  1059. if len(search_names) > 1:
  1060. file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.mp4").replace(
  1061. "\\", "/")
  1062. if os.path.isfile(file_path):
  1063. file_found = True
  1064.  
  1065. if not file_found and os.path.exists(main_folder_path):
  1066. file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.mp4").replace("\\", "/")
  1067. if os.path.isfile(file_path):
  1068. file_found = True
  1069. else:
  1070. if len(search_names) > 1:
  1071. file_path = os.path.join(main_folder_path, f"{search_names[1]}.mp4").replace("\\", "/")
  1072. if os.path.isfile(file_path):
  1073. file_found = True
  1074.  
  1075. if not file_found:
  1076. all_si = False
  1077. break
  1078.  
  1079. elif filter_name == "Manual":
  1080. # Logic for manuals
  1081. main_folder_path = os.path.join(self.launchbox_path, "Manuals",
  1082. self.platform_combobox.get()).replace("\\", "/")
  1083. alternative_folder_path = os.path.join(self.alternative_path, self.platform_combobox.get(),
  1084. "Manuals").replace("\\",
  1085. "/") if self.alternative_path else None
  1086.  
  1087. file_found = False
  1088. if alternative_folder_path and os.path.exists(alternative_folder_path):
  1089. file_path = os.path.join(alternative_folder_path, f"{search_names[0]}-01.pdf").replace("\\",
  1090. "/")
  1091. if os.path.isfile(file_path):
  1092. file_found = True
  1093. else:
  1094. if len(search_names) > 1:
  1095. file_path = os.path.join(alternative_folder_path, f"{search_names[1]}.pdf").replace(
  1096. "\\", "/")
  1097. if os.path.isfile(file_path):
  1098. file_found = True
  1099.  
  1100. if not file_found and os.path.exists(main_folder_path):
  1101. file_path = os.path.join(main_folder_path, f"{search_names[0]}-01.pdf").replace("\\", "/")
  1102. if os.path.isfile(file_path):
  1103. file_found = True
  1104. else:
  1105. if len(search_names) > 1:
  1106. file_path = os.path.join(main_folder_path, f"{search_names[1]}.pdf").replace("\\", "/")
  1107. if os.path.isfile(file_path):
  1108. file_found = True
  1109.  
  1110. if not file_found:
  1111. all_si = False
  1112. break
  1113.  
  1114. else:
  1115. # Logic for images
  1116. image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(),
  1117. filter_name).replace("\\", "/")
  1118. image_found = False
  1119. if os.path.exists(image_folder):
  1120. for ext in ["png", "jpg"]:
  1121. image_path = os.path.join(image_folder, f"{search_names[0]}-01.{ext}").replace("\\", "/")
  1122. if os.path.isfile(image_path):
  1123. image_found = True
  1124. break
  1125. if len(search_names) > 1:
  1126. image_path = os.path.join(image_folder, f"{search_names[1]}.{ext}").replace("\\", "/")
  1127. if os.path.isfile(image_path):
  1128. image_found = True
  1129. break
  1130.  
  1131. if not image_found:
  1132. all_si = False
  1133. break
  1134.  
  1135. if not all_si:
  1136. filtered_game_dict[title] = search_names
  1137.  
  1138. return filtered_game_dict
  1139.  
  1140. def get_filter_value(self, search_names, filter_name):
  1141. image_folder = os.path.join(self.launchbox_path, "Images", self.platform_combobox.get(), filter_name).replace(
  1142. "\\", "/")
  1143. if os.path.exists(image_folder):
  1144. for search_name in search_names:
  1145. for ext in ["png", "jpg"]:
  1146. image_path = os.path.join(image_folder, f"{search_name}.{ext}").replace("\\", "/")
  1147. if os.path.isfile(image_path):
  1148. return search_name
  1149. return ""
  1150.  
  1151. if __name__ == "__main__":
  1152. try:
  1153. root = tk.Tk()
  1154. app = LaunchBoxManager(root)
  1155. root.mainloop()
  1156. except MemoryError:
  1157. print("Severe memory error. The program must close.")
  1158. except Exception as e:
  1159. print(f"Unexpected error: {e}")
  1160. messagebox.showerror("Error", f"An unexpected error occurred: {e}")
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement