Guest User

Plex Database Editor GUI 2.1 (edited for Windows)

a guest
Oct 24th, 2025
13
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 36.61 KB | Source Code | 0 0
  1. #!/usr/bin/env python3
  2. """
  3. Plex Database Editor - GUI Version 2.1 Fixed
  4. Clean interface with human-readable dates and configurable paths
  5. Fixed double-click editing and window grab issues
  6. """
  7.  
  8. import tkinter as tk
  9. from tkinter import ttk, messagebox, filedialog
  10. import platform
  11. import subprocess
  12. import os
  13. import sys
  14. from datetime import datetime, timedelta
  15. import shutil
  16. import json
  17. from pathlib import Path
  18.  
  19. class PlexDatabaseEditor:
  20. def __init__(self, root):
  21. self.root = root
  22. self.root.title("Plex Database Editor v2.1")
  23. self.root.geometry("1000x700")
  24.  
  25. # Configuration file path
  26. self.config_file = os.path.expanduser("~/.plex_editor_config.json")
  27. self.load_config()
  28.  
  29. # Check if running as administrator (Windows) or root (Linux/Mac)
  30. def is_admin():
  31. if os.name == "nt": # Windows
  32. import ctypes
  33. try:
  34. return ctypes.windll.shell32.IsUserAnAdmin()
  35. except:
  36. return False
  37. else: # Unix/Linux
  38. return os.geteuid() == 0
  39.  
  40. # Use it here:
  41. if not is_admin():
  42. response = messagebox.askyesno("Permission Required",
  43. "This application should be run as Administrator (or with sudo) for full access.\n"
  44. "Continue anyway? (some features may not work)")
  45. if not response:
  46. sys.exit(1)
  47.  
  48. self.movies = []
  49. self.setup_ui()
  50.  
  51. # Validate paths before proceeding
  52. if self.validate_paths():
  53. self.check_plex_status()
  54. self.load_movies()
  55. else:
  56. self.show_settings()
  57.  
  58. def load_config(self):
  59. """Load configuration from file or use defaults"""
  60. default_config = {
  61. "plex_sqlite": "/usr/lib/plexmediaserver/Plex SQLite",
  62. "db_path": "/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db",
  63. "date_format": "%Y-%m-%d %H:%M:%S"
  64. }
  65.  
  66. if os.path.exists(self.config_file):
  67. try:
  68. with open(self.config_file, 'r') as f:
  69. config = json.load(f)
  70. self.plex_sqlite = config.get("plex_sqlite", default_config["plex_sqlite"])
  71. self.db_path = config.get("db_path", default_config["db_path"])
  72. self.date_format = config.get("date_format", default_config["date_format"])
  73. except Exception as e:
  74. print(f"Error loading config: {e}")
  75. self.plex_sqlite = default_config["plex_sqlite"]
  76. self.db_path = default_config["db_path"]
  77. self.date_format = default_config["date_format"]
  78. else:
  79. self.plex_sqlite = default_config["plex_sqlite"]
  80. self.db_path = default_config["db_path"]
  81. self.date_format = default_config["date_format"]
  82.  
  83. def save_config(self):
  84. """Save configuration to file"""
  85. config = {
  86. "plex_sqlite": self.plex_sqlite,
  87. "db_path": self.db_path,
  88. "date_format": self.date_format
  89. }
  90. try:
  91. with open(self.config_file, 'w') as f:
  92. json.dump(config, f, indent=2)
  93. return True
  94. except Exception as e:
  95. messagebox.showerror("Config Error", f"Failed to save config: {e}")
  96. return False
  97.  
  98. def validate_paths(self):
  99. """Check if configured paths exist"""
  100. if not os.path.exists(self.plex_sqlite):
  101. return False
  102. if not os.path.exists(self.db_path):
  103. return False
  104. return True
  105.  
  106. def setup_ui(self):
  107. """Create the user interface"""
  108. # Menu bar
  109. menubar = tk.Menu(self.root)
  110. self.root.config(menu=menubar)
  111.  
  112. # File menu
  113. file_menu = tk.Menu(menubar, tearoff=0)
  114. menubar.add_cascade(label="File", menu=file_menu)
  115. file_menu.add_command(label="Settings", command=self.show_settings, accelerator="Ctrl+,")
  116. file_menu.add_command(label="Create Backup", command=self.create_backup, accelerator="Ctrl+B")
  117. file_menu.add_separator()
  118. file_menu.add_command(label="Exit", command=self.root.quit, accelerator="Ctrl+Q")
  119.  
  120. # Service menu
  121. service_menu = tk.Menu(menubar, tearoff=0)
  122. menubar.add_cascade(label="Service", menu=service_menu)
  123. service_menu.add_command(label="Check Status", command=self.check_plex_status)
  124. service_menu.add_command(label="Stop Plex", command=self.stop_plex)
  125. service_menu.add_command(label="Start Plex", command=self.start_plex)
  126.  
  127. # Help menu
  128. help_menu = tk.Menu(menubar, tearoff=0)
  129. menubar.add_cascade(label="Help", menu=help_menu)
  130. help_menu.add_command(label="Date Format Help", command=self.show_date_help)
  131. help_menu.add_command(label="Keyboard Shortcuts", command=self.show_shortcuts)
  132. help_menu.add_command(label="About", command=self.show_about)
  133.  
  134. # Main container
  135. main_frame = ttk.Frame(self.root, padding="10")
  136. main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  137.  
  138. # Status bar at top
  139. self.status_label = ttk.Label(main_frame, text="Ready",
  140. relief=tk.SUNKEN, anchor=tk.W)
  141. self.status_label.grid(row=0, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  142.  
  143. # Plex service status
  144. status_frame = ttk.Frame(main_frame)
  145. status_frame.grid(row=1, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  146.  
  147. self.plex_status = ttk.Label(status_frame, text="Checking Plex status...", font=("", 10, "bold"))
  148. self.plex_status.pack(side=tk.LEFT, padx=5)
  149.  
  150. ttk.Button(status_frame, text="Stop Plex",
  151. command=self.stop_plex).pack(side=tk.LEFT, padx=5)
  152. ttk.Button(status_frame, text="Start Plex",
  153. command=self.start_plex).pack(side=tk.LEFT, padx=5)
  154. ttk.Button(status_frame, text="Create Backup",
  155. command=self.create_backup).pack(side=tk.LEFT, padx=5)
  156.  
  157. # Search and filter frame
  158. search_frame = ttk.LabelFrame(main_frame, text="Search & Filter", padding="5")
  159. search_frame.grid(row=2, column=0, columnspan=3, sticky=(tk.W, tk.E), pady=5)
  160.  
  161. ttk.Label(search_frame, text="Search:").grid(row=0, column=0, padx=5)
  162. self.search_var = tk.StringVar()
  163. self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=40)
  164. self.search_entry.grid(row=0, column=1, padx=5)
  165. self.search_entry.bind('<KeyRelease>', self.filter_movies)
  166.  
  167. ttk.Button(search_frame, text="Clear",
  168. command=self.clear_search).grid(row=0, column=2, padx=5)
  169. ttk.Button(search_frame, text="Refresh",
  170. command=self.load_movies).grid(row=0, column=3, padx=5)
  171.  
  172. # Add movie count label
  173. self.count_label = ttk.Label(search_frame, text="", font=("", 10))
  174. self.count_label.grid(row=0, column=4, padx=20)
  175.  
  176. # Movie list with scrollbar
  177. list_frame = ttk.LabelFrame(main_frame, text="Movies (Double-click date to edit)", padding="5")
  178. list_frame.grid(row=3, column=0, columnspan=3, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
  179.  
  180. # Treeview for movie list
  181. columns = ('ID', 'Title', 'Added Date', 'Epoch')
  182. self.tree = ttk.Treeview(list_frame, columns=columns, show='headings', height=20)
  183.  
  184. # Define column headings and widths
  185. self.tree.heading('ID', text='ID')
  186. self.tree.heading('Title', text='Title')
  187. self.tree.heading('Added Date', text='Added Date')
  188. self.tree.heading('Epoch', text='Epoch (Hidden)')
  189.  
  190. self.tree.column('ID', width=60)
  191. self.tree.column('Title', width=400)
  192. self.tree.column('Added Date', width=200)
  193. self.tree.column('Epoch', width=0, stretch=False) # Hidden column
  194.  
  195. # Hide the epoch column
  196. self.tree['displaycolumns'] = ('ID', 'Title', 'Added Date')
  197.  
  198. # Scrollbar
  199. scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.tree.yview)
  200. self.tree.configure(yscrollcommand=scrollbar.set)
  201.  
  202. self.tree.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
  203. scrollbar.grid(row=0, column=1, sticky=(tk.N, tk.S))
  204.  
  205. # Double-click to edit - bind to the treeview
  206. self.tree.bind('<Double-Button-1>', self.edit_date)
  207.  
  208. # Info label at bottom
  209. self.info_label = ttk.Label(main_frame,
  210. text="Double-click a date to edit. Date format: " + self.date_format,
  211. font=("", 9), foreground="gray")
  212. self.info_label.grid(row=4, column=0, columnspan=3, pady=5)
  213.  
  214. # Keyboard bindings
  215. self.root.bind('<Control-comma>', lambda e: self.show_settings())
  216. self.root.bind('<Control-b>', lambda e: self.create_backup())
  217. self.root.bind('<Control-q>', lambda e: self.root.quit())
  218. self.root.bind('<Control-r>', lambda e: self.load_movies())
  219. self.root.bind('<Control-f>', lambda e: self.search_entry.focus())
  220.  
  221. # Configure style
  222. style = ttk.Style()
  223. style.configure("Accent.TButton", font=("", 10, "bold"))
  224.  
  225. # Make grid expandable
  226. self.root.columnconfigure(0, weight=1)
  227. self.root.rowconfigure(0, weight=1)
  228. main_frame.columnconfigure(0, weight=1)
  229. main_frame.rowconfigure(3, weight=1)
  230. list_frame.columnconfigure(0, weight=1)
  231. list_frame.rowconfigure(0, weight=1)
  232.  
  233. def execute_sql(self, query):
  234. """Execute SQL using Plex SQLite"""
  235. try:
  236. print(f"\n=== Executing SQL ===")
  237. print(f"Command: {self.plex_sqlite}")
  238. print(f"Database: {self.db_path}")
  239. print(f"Query: {query}")
  240.  
  241. result = subprocess.run(
  242. [self.plex_sqlite, self.db_path, query],
  243. capture_output=True,
  244. text=True,
  245. check=True
  246. )
  247.  
  248. print(f"Return code: {result.returncode}")
  249. print(f"Stdout: '{result.stdout}'")
  250. print(f"Stderr: '{result.stderr}'")
  251. print(f"=== End SQL ===\n")
  252.  
  253. return result.stdout
  254. except subprocess.CalledProcessError as e:
  255. print(f"!!! SQL ERROR !!!")
  256. print(f"Return code: {e.returncode}")
  257. print(f"Stderr: {e.stderr}")
  258. messagebox.showerror("SQL Error", f"Error executing query:\n{e.stderr}")
  259. return None
  260.  
  261. def check_plex_status(self):
  262. """Check if Plex is running (cross-platform)"""
  263. system = platform.system().lower()
  264.  
  265. if system == "windows":
  266. # Use tasklist to see if Plex Media Server.exe is running
  267. try:
  268. result = subprocess.run(
  269. ["tasklist", "/FI", "IMAGENAME eq Plex Media Server.exe"],
  270. capture_output=True, text=True
  271. )
  272. if "Plex Media Server.exe" in result.stdout:
  273. self.plex_status.config(text="WARNING: Plex is RUNNING", foreground="orange")
  274. return True
  275. else:
  276. self.plex_status.config(text="OK: Plex is STOPPED", foreground="green")
  277. return False
  278. except Exception as e:
  279. self.plex_status.config(text=f"Error checking status: {e}", foreground="red")
  280. return False
  281. else:
  282. # Linux/macOS version
  283. result = subprocess.run(['pgrep', '-x', 'Plex Media Serv'], capture_output=True)
  284. if result.returncode == 0:
  285. self.plex_status.config(text="WARNING: Plex is RUNNING", foreground="orange")
  286. return True
  287. else:
  288. self.plex_status.config(text="OK: Plex is STOPPED", foreground="green")
  289. return False
  290.  
  291. def stop_plex(self):
  292. """Stop Plex service"""
  293. self.status_label.config(text="Stopping Plex...")
  294. system = platform.system().lower()
  295.  
  296. try:
  297. if system == "windows":
  298. subprocess.run(["taskkill", "/F", "/IM", "Plex Media Server.exe"], check=False)
  299. else:
  300. subprocess.run(['systemctl', 'stop', 'plexmediaserver'])
  301. self.check_plex_status()
  302. self.status_label.config(text="Plex stopped")
  303. except Exception as e:
  304. messagebox.showerror("Error", f"Failed to stop Plex: {e}")
  305.  
  306. def start_plex(self):
  307. """Start Plex service"""
  308. self.status_label.config(text="Starting Plex...")
  309. system = platform.system().lower()
  310.  
  311. try:
  312. if system == "windows":
  313. # Replace this path if Plex is installed somewhere else
  314. plex_path = r"C:\Program Files\Plex\Plex Media Server\Plex Media Server.exe"
  315. subprocess.Popen([plex_path], shell=True)
  316. else:
  317. subprocess.run(['systemctl', 'start', 'plexmediaserver'])
  318. self.check_plex_status()
  319. self.status_label.config(text="Plex started")
  320. except Exception as e:
  321. messagebox.showerror("Error", f"Failed to start Plex: {e}")
  322.  
  323. def create_backup(self):
  324. """Create database backup"""
  325. timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  326. backup_dir = os.path.dirname(self.db_path)
  327. backup_path = os.path.join(backup_dir, f"com.plexapp.plugins.library.db.backup.{timestamp}")
  328.  
  329. try:
  330. shutil.copy2(self.db_path, backup_path)
  331. messagebox.showinfo("Backup Created", f"Backup saved to:\n{backup_path}")
  332. self.status_label.config(text=f"Backup created: {os.path.basename(backup_path)}")
  333. except Exception as e:
  334. messagebox.showerror("Backup Error", f"Failed to create backup:\n{e}")
  335.  
  336. def load_movies(self):
  337. """Load movies from database"""
  338. self.status_label.config(text="Loading movies...")
  339.  
  340. query = """
  341. SELECT id, title, added_at
  342. FROM metadata_items
  343. WHERE guid LIKE 'plex://movie/%'
  344. ORDER BY added_at DESC
  345. LIMIT 1000
  346. """
  347.  
  348. result = self.execute_sql(query)
  349. if result:
  350. # Clear existing items
  351. for item in self.tree.get_children():
  352. self.tree.delete(item)
  353.  
  354. self.movies = []
  355. lines = result.strip().split('\n')
  356. for line in lines:
  357. if '|' in line:
  358. parts = line.split('|')
  359. if len(parts) >= 3 and parts[0].isdigit():
  360. movie_id = int(parts[0])
  361. title = parts[1]
  362. epoch = int(parts[2]) if parts[2].isdigit() else 0
  363.  
  364. # Convert epoch to human-readable date
  365. try:
  366. date_str = datetime.fromtimestamp(epoch).strftime(self.date_format)
  367. except:
  368. date_str = 'Invalid Date'
  369.  
  370. self.movies.append({
  371. 'id': movie_id,
  372. 'title': title,
  373. 'epoch': epoch,
  374. 'date': date_str
  375. })
  376.  
  377. self.display_movies(self.movies)
  378. self.status_label.config(text=f"Loaded {len(self.movies)} movies")
  379. self.count_label.config(text=f"Total: {len(self.movies)}")
  380.  
  381. def display_movies(self, movies):
  382. """Display movies in treeview"""
  383. # Clear existing
  384. for item in self.tree.get_children():
  385. self.tree.delete(item)
  386.  
  387. # Add movies
  388. for movie in movies:
  389. self.tree.insert('', 'end', values=(
  390. movie['id'],
  391. movie['title'],
  392. movie['date'],
  393. movie['epoch'] # Hidden column but still in values
  394. ))
  395.  
  396. def filter_movies(self, event=None):
  397. """Filter movies based on search"""
  398. search_term = self.search_var.get().lower()
  399. if not search_term:
  400. self.display_movies(self.movies)
  401. self.count_label.config(text=f"Total: {len(self.movies)}")
  402. else:
  403. filtered = [m for m in self.movies if search_term in m['title'].lower()]
  404. self.display_movies(filtered)
  405. self.count_label.config(text=f"Showing: {len(filtered)} of {len(self.movies)}")
  406. self.status_label.config(text=f"Filtered to {len(filtered)} movies")
  407.  
  408. def clear_search(self):
  409. """Clear search field"""
  410. self.search_var.set("")
  411. self.display_movies(self.movies)
  412. self.count_label.config(text=f"Total: {len(self.movies)}")
  413. self.status_label.config(text=f"Showing all {len(self.movies)} movies")
  414.  
  415. def edit_date(self, event):
  416. """Edit date for selected movie"""
  417. print("\n*** DOUBLE-CLICK DETECTED ***")
  418.  
  419. # Get the item that was clicked
  420. region = self.tree.identify_region(event.x, event.y)
  421. print(f"Click region: {region}")
  422. if region != "cell":
  423. print("Not a cell, ignoring")
  424. return
  425.  
  426. # Get the item
  427. item_id = self.tree.identify_row(event.y)
  428. print(f"Item ID: {item_id}")
  429. if not item_id:
  430. print("No item found")
  431. return
  432.  
  433. # Get the values from the clicked item
  434. item = self.tree.item(item_id)
  435. values = item.get('values')
  436. print(f"Values: {values}")
  437. if not values or len(values) < 4:
  438. print(f"Error: Invalid values from tree item: {values}")
  439. return
  440.  
  441. try:
  442. movie_id = values[0]
  443. title = str(values[1])
  444. current_date = str(values[2])
  445. current_epoch = values[3]
  446.  
  447. print(f"Calling create_edit_dialog for movie: {title}")
  448. # Create edit dialog
  449. self.create_edit_dialog(movie_id, title, current_date, current_epoch)
  450.  
  451. except Exception as e:
  452. print(f"Error in edit_date: {e}")
  453. messagebox.showerror("Error", f"Failed to open edit dialog: {e}")
  454.  
  455. def create_edit_dialog(self, movie_id, title, current_date, current_epoch):
  456. """Create the date edit dialog"""
  457. print("\n" + "="*60)
  458. print("OPENING EDIT DIALOG")
  459. print(f"Movie ID: {movie_id}")
  460. print(f"Title: {title}")
  461. print(f"Current Date: {current_date}")
  462. print(f"Current Epoch: {current_epoch}")
  463. print("="*60 + "\n")
  464.  
  465. dialog = tk.Toplevel(self.root)
  466. dialog.title("Edit Date")
  467. print("Dialog window created")
  468.  
  469. # Build the dialog content first
  470.  
  471. # On-screen status message (visible even without console)
  472. status_frame = ttk.Frame(dialog, relief=tk.SUNKEN, borderwidth=1)
  473. status_frame.pack(fill='x', padx=10, pady=5)
  474. status_msg = ttk.Label(status_frame,
  475. text="Dialog opened successfully. Look for SAVE CHANGES button at bottom.",
  476. font=("", 9), foreground="blue", wraplength=500)
  477. status_msg.pack(pady=3)
  478.  
  479. # Simple warning text at top (no emoji or background color to avoid X11 errors)
  480. warning_label = ttk.Label(dialog,
  481. text="WARNING: Make sure Plex Media Server is STOPPED before saving changes",
  482. font=("", 9, "bold"), foreground="red")
  483. warning_label.pack(pady=10)
  484.  
  485. # Movie info
  486. ttk.Label(dialog, text=f"Movie: {title}", font=("", 11, "bold")).pack(pady=10)
  487. ttk.Label(dialog, text=f"Current Date: {current_date}").pack(pady=5)
  488.  
  489. # Date input frame
  490. input_frame = ttk.Frame(dialog)
  491. input_frame.pack(pady=20)
  492.  
  493. ttk.Label(input_frame, text="New Date:").grid(row=0, column=0, padx=5, pady=5)
  494.  
  495. # Date entry with current value
  496. date_var = tk.StringVar(value=current_date)
  497. date_entry = ttk.Entry(input_frame, textvariable=date_var, width=30, font=("", 11))
  498. date_entry.grid(row=0, column=1, padx=5, pady=5)
  499.  
  500. # Format help
  501. format_label = ttk.Label(input_frame, text=f"Format: {self.date_format}",
  502. font=("", 9), foreground="gray")
  503. format_label.grid(row=1, column=1, padx=5, pady=2)
  504.  
  505. # Quick date buttons
  506. quick_frame = ttk.LabelFrame(dialog, text="Quick Dates", padding="10")
  507. quick_frame.pack(pady=10, padx=20, fill='x')
  508.  
  509. button_frame = ttk.Frame(quick_frame)
  510. button_frame.pack()
  511.  
  512. def set_date(date_str):
  513. date_var.set(date_str)
  514.  
  515. # Quick date options
  516. now = datetime.now()
  517. dates = [
  518. ("Now", now.strftime(self.date_format)),
  519. ("Today", now.replace(hour=0, minute=0, second=0).strftime(self.date_format)),
  520. ("Yesterday", (now - timedelta(days=1)).strftime(self.date_format)),
  521. ("1 Week Ago", (now - timedelta(weeks=1)).strftime(self.date_format)),
  522. ("1 Month Ago", (now - timedelta(days=30)).strftime(self.date_format)),
  523. ("1 Year Ago", (now - timedelta(days=365)).strftime(self.date_format))
  524. ]
  525.  
  526. row = 0
  527. col = 0
  528. for label, date_val in dates:
  529. btn = ttk.Button(button_frame, text=label,
  530. command=lambda d=date_val: set_date(d))
  531. btn.grid(row=row, column=col, padx=2, pady=2)
  532. col += 1
  533. if col > 2: # 3 buttons per row
  534. col = 0
  535. row += 1
  536.  
  537. # Result label
  538. result_label = ttk.Label(dialog, text="", foreground="green")
  539. result_label.pack(pady=5)
  540.  
  541. def save_edit():
  542. new_date_str = date_var.get()
  543.  
  544. # Update on-screen status
  545. status_msg.config(text="Checking if Plex is running...", foreground="blue")
  546. dialog.update()
  547.  
  548. # Check if Plex is running
  549. if self.check_plex_status():
  550. status_msg.config(text="ERROR: Plex is running! Stop Plex first, then try again.", foreground="red")
  551. result_label.config(text="WARNING: Plex is running! Stop Plex first.", foreground="orange")
  552. response = messagebox.askyesno("Plex is Running",
  553. "Plex Media Server is currently running.\n"
  554. "The database may be locked.\n\n"
  555. "Do you want to stop Plex and try the update?")
  556. if response:
  557. self.stop_plex()
  558. status_msg.config(text="Plex stopped. Click SAVE CHANGES again.", foreground="green")
  559. result_label.config(text="Plex stopped. Click Save again to update.", foreground="blue")
  560. return
  561.  
  562. status_msg.config(text="Plex is stopped. Processing update...", foreground="blue")
  563. dialog.update()
  564.  
  565. try:
  566. # Parse the date string to epoch
  567. new_datetime = datetime.strptime(new_date_str, self.date_format)
  568. new_epoch = int(new_datetime.timestamp())
  569.  
  570. print(f"\n=== UPDATE DEBUG ===")
  571. print(f"Movie ID: {movie_id}")
  572. print(f"Title: {title}")
  573. print(f"Old epoch: {current_epoch}")
  574. print(f"New epoch: {new_epoch}")
  575. print(f"Old date: {current_date}")
  576. print(f"New date: {new_date_str}")
  577.  
  578. status_msg.config(text=f"Updating database for movie ID {movie_id}...", foreground="blue")
  579. dialog.update()
  580.  
  581. # Update the database
  582. query = f"UPDATE metadata_items SET added_at = {new_epoch} WHERE id = {movie_id}"
  583. print(f"Query: {query}")
  584.  
  585. result = self.execute_sql(query)
  586. print(f"Query result: '{result}'")
  587.  
  588. if result is not None: # Check if not None (UPDATE returns empty string on success)
  589. status_msg.config(text="Update sent. Verifying...", foreground="blue")
  590. dialog.update()
  591.  
  592. # Verify the update actually took effect
  593. verify_query = f"SELECT added_at FROM metadata_items WHERE id = {movie_id}"
  594. verify_result = self.execute_sql(verify_query)
  595. print(f"Verification query result: '{verify_result}'")
  596.  
  597. if verify_result and str(new_epoch) in verify_result:
  598. status_msg.config(text="SUCCESS! Date updated and verified in database!", foreground="green")
  599. result_label.config(text="SUCCESS: Date updated and verified!", foreground="green")
  600. print("SUCCESS: Update verified successfully!")
  601. else:
  602. status_msg.config(text=f"WARNING: Update unclear. Expected {new_epoch}, got {verify_result}", foreground="orange")
  603. result_label.config(text="WARNING: Update completed but verification unclear", foreground="orange")
  604. print(f"WARNING: Verification unclear. Expected {new_epoch}, got: {verify_result}")
  605.  
  606. self.load_movies()
  607. dialog.after(2000, dialog.destroy)
  608. self.status_label.config(text=f"Updated date for movie ID {movie_id}")
  609. else:
  610. status_msg.config(text="ERROR: Database update failed!", foreground="red")
  611. result_label.config(text="ERROR: Update failed - database error", foreground="red")
  612. print("ERROR: Update failed - execute_sql returned None")
  613.  
  614. except ValueError as e:
  615. status_msg.config(text=f"ERROR: Invalid date format!", foreground="red")
  616. result_label.config(text=f"ERROR: Invalid date format. Use: {self.date_format}", foreground="red")
  617. print(f"ValueError: {e}")
  618. except Exception as e:
  619. status_msg.config(text=f"ERROR: {str(e)}", foreground="red")
  620. result_label.config(text=f"ERROR: {str(e)}", foreground="red")
  621. print(f"Exception: {e}")
  622.  
  623. # Buttons - make them VERY prominent with lots of spacing
  624. button_frame = ttk.Frame(dialog)
  625. button_frame.pack(pady=30, padx=20)
  626.  
  627. # Create a custom style for the save button (no emoji to avoid X11 errors)
  628. save_btn = ttk.Button(button_frame, text="SAVE CHANGES", command=save_edit,
  629. style="Accent.TButton", width=25)
  630. save_btn.pack(side=tk.LEFT, padx=15, pady=10)
  631.  
  632. cancel_btn = ttk.Button(button_frame, text="Cancel",
  633. command=dialog.destroy, width=20)
  634. cancel_btn.pack(side=tk.LEFT, padx=15, pady=10)
  635.  
  636. print("Buttons created: SAVE CHANGES and Cancel")
  637.  
  638. # Add another status label at the very bottom to make sure it's visible
  639. bottom_status = ttk.Label(dialog,
  640. text="Scroll down if you don't see the SAVE CHANGES button above",
  641. font=("", 8), foreground="gray")
  642. bottom_status.pack(pady=5)
  643.  
  644. # Bind Enter key to save
  645. date_entry.bind('<Return>', lambda e: save_edit())
  646. dialog.bind('<Escape>', lambda e: dialog.destroy())
  647.  
  648. # Set dialog properties after content is added
  649. dialog.geometry("600x550") # Much larger to ensure buttons are visible
  650. dialog.transient(self.root)
  651. dialog.resizable(True, True) # Make it resizable
  652.  
  653. # Center the dialog
  654. dialog.update_idletasks()
  655. x = (dialog.winfo_screenwidth() // 2) - (300)
  656. y = (dialog.winfo_screenheight() // 2) - (275)
  657. dialog.geometry(f"600x550+{x}+{y}")
  658.  
  659. # Now try to grab focus after window is fully built
  660. dialog.after(100, lambda: dialog.grab_set_safe())
  661.  
  662. # Safe grab method
  663. def grab_set_safe():
  664. try:
  665. dialog.grab_set()
  666. except tk.TclError:
  667. pass # Ignore if grab fails
  668.  
  669. dialog.grab_set_safe = grab_set_safe
  670.  
  671. # Focus the entry field
  672. date_entry.select_range(0, tk.END)
  673. date_entry.focus()
  674.  
  675. print("Dialog setup complete - should be visible now")
  676. print("="*60 + "\n")
  677.  
  678. def show_settings(self):
  679. """Show settings dialog"""
  680. dialog = tk.Toplevel(self.root)
  681. dialog.title("Settings")
  682. dialog.geometry("700x400")
  683. dialog.transient(self.root)
  684.  
  685. # Center the dialog
  686. dialog.update_idletasks()
  687. x = (dialog.winfo_screenwidth() // 2) - (350)
  688. y = (dialog.winfo_screenheight() // 2) - (200)
  689. dialog.geometry(f"700x400+{x}+{y}")
  690.  
  691. ttk.Label(dialog, text="Configuration Settings",
  692. font=("", 14, "bold")).pack(pady=10)
  693.  
  694. # Settings frame
  695. settings_frame = ttk.Frame(dialog, padding="20")
  696. settings_frame.pack(fill='both', expand=True)
  697.  
  698. # Plex SQLite path
  699. ttk.Label(settings_frame, text="Plex SQLite Path:").grid(row=0, column=0, sticky='w', pady=5)
  700. sqlite_var = tk.StringVar(value=self.plex_sqlite)
  701. sqlite_entry = ttk.Entry(settings_frame, textvariable=sqlite_var, width=60)
  702. sqlite_entry.grid(row=0, column=1, pady=5, padx=5)
  703.  
  704. def browse_sqlite():
  705. filename = filedialog.askopenfilename(
  706. title="Select Plex SQLite executable",
  707. initialdir="/usr/lib/plexmediaserver/"
  708. )
  709. if filename:
  710. sqlite_var.set(filename)
  711.  
  712. ttk.Button(settings_frame, text="Browse",
  713. command=browse_sqlite).grid(row=0, column=2, pady=5)
  714.  
  715. # Database path
  716. ttk.Label(settings_frame, text="Database Path:").grid(row=1, column=0, sticky='w', pady=5)
  717. db_var = tk.StringVar(value=self.db_path)
  718. db_entry = ttk.Entry(settings_frame, textvariable=db_var, width=60)
  719. db_entry.grid(row=1, column=1, pady=5, padx=5)
  720.  
  721. def browse_db():
  722. filename = filedialog.askopenfilename(
  723. title="Select Plex database",
  724. initialdir="/var/lib/plexmediaserver/",
  725. filetypes=[("Database files", "*.db"), ("All files", "*.*")]
  726. )
  727. if filename:
  728. db_var.set(filename)
  729.  
  730. ttk.Button(settings_frame, text="Browse",
  731. command=browse_db).grid(row=1, column=2, pady=5)
  732.  
  733. # Date format
  734. ttk.Label(settings_frame, text="Date Format:").grid(row=2, column=0, sticky='w', pady=5)
  735. format_var = tk.StringVar(value=self.date_format)
  736. format_combo = ttk.Combobox(settings_frame, textvariable=format_var, width=30)
  737. format_combo['values'] = (
  738. '%Y-%m-%d %H:%M:%S',
  739. '%Y/%m/%d %H:%M:%S',
  740. '%d-%m-%Y %H:%M:%S',
  741. '%d/%m/%Y %H:%M:%S',
  742. '%m/%d/%Y %H:%M:%S',
  743. '%Y-%m-%d',
  744. '%m/%d/%Y',
  745. '%d/%m/%Y'
  746. )
  747. format_combo.grid(row=2, column=1, pady=5, padx=5, sticky='w')
  748.  
  749. # Current status
  750. status_frame = ttk.LabelFrame(settings_frame, text="Current Status", padding="10")
  751. status_frame.grid(row=3, column=0, columnspan=3, pady=20, sticky='ew')
  752.  
  753. sqlite_exists = "[OK] Found" if os.path.exists(sqlite_var.get()) else "[ERROR] Not found"
  754. db_exists = "[OK] Found" if os.path.exists(db_var.get()) else "[ERROR] Not found"
  755.  
  756. ttk.Label(status_frame, text=f"Plex SQLite: {sqlite_exists}").pack(anchor='w')
  757. ttk.Label(status_frame, text=f"Database: {db_exists}").pack(anchor='w')
  758.  
  759. # Buttons
  760. button_frame = ttk.Frame(dialog)
  761. button_frame.pack(pady=10)
  762.  
  763. def save_settings():
  764. self.plex_sqlite = sqlite_var.get()
  765. self.db_path = db_var.get()
  766. self.date_format = format_var.get()
  767.  
  768. if not os.path.exists(self.plex_sqlite):
  769. messagebox.showwarning("Warning", "Plex SQLite executable not found at specified path")
  770. if not os.path.exists(self.db_path):
  771. messagebox.showwarning("Warning", "Database file not found at specified path")
  772.  
  773. if self.save_config():
  774. messagebox.showinfo("Settings", "Settings saved successfully!")
  775. dialog.destroy()
  776.  
  777. # Reload if paths are valid
  778. if self.validate_paths():
  779. self.load_movies()
  780. self.info_label.config(text="Double-click a date to edit. Date format: " + self.date_format)
  781.  
  782. def reset_defaults():
  783. sqlite_var.set("/usr/lib/plexmediaserver/Plex SQLite")
  784. db_var.set("/var/lib/plexmediaserver/Library/Application Support/Plex Media Server/Plug-in Support/Databases/com.plexapp.plugins.library.db")
  785. format_var.set("%Y-%m-%d %H:%M:%S")
  786.  
  787. ttk.Button(button_frame, text="Save", command=save_settings,
  788. style="Accent.TButton").pack(side=tk.LEFT, padx=5)
  789. ttk.Button(button_frame, text="Reset Defaults",
  790. command=reset_defaults).pack(side=tk.LEFT, padx=5)
  791. ttk.Button(button_frame, text="Cancel",
  792. command=dialog.destroy).pack(side=tk.LEFT, padx=5)
  793.  
  794. # Try to grab after a delay
  795. dialog.after(100, lambda: self.safe_grab(dialog))
  796.  
  797. def safe_grab(self, window):
  798. """Safely try to grab a window"""
  799. try:
  800. window.grab_set()
  801. except tk.TclError:
  802. pass # Ignore grab errors
  803.  
  804. def show_date_help(self):
  805. """Show date format help"""
  806. help_text = """Date Format Codes:
  807.  
  808. %Y - 4-digit year (2024)
  809. %y - 2-digit year (24)
  810. %m - Month as number (01-12)
  811. %B - Full month name (January)
  812. %b - Abbreviated month (Jan)
  813. %d - Day of month (01-31)
  814. %H - Hour (00-23)
  815. %I - Hour (01-12)
  816. %M - Minute (00-59)
  817. %S - Second (00-59)
  818. %p - AM/PM
  819.  
  820. Examples:
  821. %Y-%m-%d %H:%M:%S → 2024-10-08 14:30:45
  822. %m/%d/%Y %I:%M %p → 10/08/2024 02:30 PM
  823. %d-%b-%Y → 08-Oct-2024"""
  824.  
  825. messagebox.showinfo("Date Format Help", help_text)
  826.  
  827. def show_shortcuts(self):
  828. """Show keyboard shortcuts"""
  829. shortcuts = """Keyboard Shortcuts:
  830.  
  831. Ctrl+, - Open Settings
  832. Ctrl+B - Create Backup
  833. Ctrl+F - Focus Search
  834. Ctrl+R - Refresh Movies
  835. Ctrl+Q - Quit Application
  836.  
  837. In Dialogs:
  838. Enter - Save/Confirm
  839. Escape - Cancel/Close"""
  840.  
  841. messagebox.showinfo("Keyboard Shortcuts", shortcuts)
  842.  
  843. def show_about(self):
  844. """Show about dialog"""
  845. about_text = """Plex Database Editor v2.1
  846.  
  847. Safely modify Plex movie timestamps using
  848. the official Plex SQLite executable.
  849.  
  850. Features:
  851. • Human-readable date editing
  852. • Configurable paths and date formats
  853. • Automatic backups
  854. • Real-time search and filtering
  855. • Keyboard shortcuts
  856.  
  857. Created for easy Plex database management."""
  858.  
  859. messagebox.showinfo("About", about_text)
  860.  
  861. if __name__ == "__main__":
  862. # Check if tkinter is available
  863. try:
  864. root = tk.Tk()
  865. app = PlexDatabaseEditor(root)
  866. root.mainloop()
  867. except Exception as e:
  868. print(f"Error starting GUI: {e}")
  869. print("\nMake sure you have tkinter installed:")
  870. print(" sudo apt-get install python3-tk")
  871. sys.exit(1)
Tags: Plex
Add Comment
Please, Sign In to add comment