#!/usr/bin/env python3 """ Native GUI using tkinter to generate One Piece progress reports without a web server. """ import os import threading import tkinter as tk from tkinter import ttk, messagebox, scrolledtext from datetime import datetime # Reuse helper logic from app.py from app import current_week, format_date_range, get_latest_sunday, get_previous_sunday from clients import jellyfin def generate_report(ui): def task(): ui['status_var'].set("Fetching...") jellyfin.clear_debug_logs() try: latest = get_latest_sunday() previous = get_previous_sunday() latest_dt = datetime.fromisoformat(latest.replace("Z", "+00:00")) previous_dt = datetime.fromisoformat(previous.replace("Z", "+00:00")) user_id = os.environ.get("JELLYFIN_USER_ID") if not user_id: raise RuntimeError("JELLYFIN_USER_ID not set in environment") ep_info = jellyfin.find_last_one_piece_episode(latest_dt, user_id) prev_ep_info = jellyfin.find_last_one_piece_before_previous_sunday(previous_dt, user_id) previous_episode = prev_ep_info["episode_number"] total_episodes = int(os.environ.get("TOTAL_EPISODES", 1155)) watched_this_week = ep_info["episode_number"] - previous_episode week_start, week_end, _ = current_week() date_range = format_date_range(week_start, week_end) report_text = ( f"ONE PIECE PROGRESS!!! ({date_range})\n" f"Episode {ep_info['episode_number']}/{total_episodes}\n" f"Around {watched_this_week} episodes watched this week!" ) debug_output = jellyfin.get_debug_logs() ui['root'].after(0, lambda: update_ui(ui, ep_info, previous_episode, watched_this_week, total_episodes, report_text, debug_output)) except Exception as e: debug_output = jellyfin.get_debug_logs() ui['root'].after(0, lambda: ui['status_var'].set(f"Error: {e}\n{debug_output}")) threading.Thread(target=task, daemon=True).start() def update_ui(ui, ep_info, previous_episode, watched_this_week, total_episodes, report_text, debug_output): ui['latest_var'].set(str(ep_info.get("episode_number", "—"))) ui['previous_var'].set(str(previous_episode)) ui['watched_var'].set(str(watched_this_week)) ui['total_var'].set(str(total_episodes)) ui['report_box'].config(state='normal') ui['report_box'].delete('1.0', tk.END) ui['report_box'].insert(tk.END, report_text) ui['report_box'].config(state='disabled') ui['debug_box'].config(state='normal') ui['debug_box'].delete('1.0', tk.END) ui['debug_box'].insert(tk.END, debug_output) ui['debug_box'].config(state='disabled') ui['status_var'].set("Done") def copy_report(ui): report = ui['report_box'].get('1.0', tk.END).strip() if not report: messagebox.showinfo("Copy", "No report to copy.") return ui['root'].clipboard_clear() ui['root'].clipboard_append(report) messagebox.showinfo("Copy", "Report copied to clipboard.") def build_ui(root): root.title("One Piece Progress Reporter") root.geometry("900x650") main = ttk.Frame(root, padding=12) main.pack(fill='both', expand=True) week_frame = ttk.Frame(main) week_frame.pack(fill='x', pady=(0,8)) week_start, week_end, _ = current_week() date_range = format_date_range(week_start, week_end) week_label = ttk.Label(week_frame, text=f"Week: {date_range} ({week_start.isoformat()} → {week_end.isoformat()})", font=("Helvetica", 12)) week_label.pack(anchor='w') info_frame = ttk.Frame(main) info_frame.pack(fill='x', pady=(6,6)) ttk.Label(info_frame, text="Last episode watched:").grid(row=0, column=0, sticky='w', padx=4, pady=2) latest_var = tk.StringVar(value="—") ttk.Label(info_frame, textvariable=latest_var).grid(row=0, column=1, sticky='w', padx=4) ttk.Label(info_frame, text="Previous week ended at:").grid(row=1, column=0, sticky='w', padx=4, pady=2) previous_var = tk.StringVar(value="—") ttk.Label(info_frame, textvariable=previous_var).grid(row=1, column=1, sticky='w', padx=4) ttk.Label(info_frame, text="Watched this week:").grid(row=2, column=0, sticky='w', padx=4, pady=2) watched_var = tk.StringVar(value="—") ttk.Label(info_frame, textvariable=watched_var).grid(row=2, column=1, sticky='w', padx=4) ttk.Label(info_frame, text="Total in series:").grid(row=3, column=0, sticky='w', padx=4, pady=2) total_var = tk.StringVar(value="—") ttk.Label(info_frame, textvariable=total_var).grid(row=3, column=1, sticky='w', padx=4) btn_frame = ttk.Frame(info_frame) btn_frame.grid(row=0, column=2, rowspan=4, padx=18) status_var = tk.StringVar(value="") generate_btn = ttk.Button(btn_frame, text="Generate Report") generate_btn.pack(fill='x', pady=(0,6)) copy_btn = ttk.Button(btn_frame, text="Copy to Clipboard") copy_btn.pack(fill='x') report_label = ttk.Label(main, text="Report:") report_label.pack(anchor='w', pady=(8,0)) report_box = scrolledtext.ScrolledText(main, height=6, wrap='word', state='disabled') report_box.pack(fill='both', expand=False, pady=(4,6)) debug_label = ttk.Label(main, text="Debug:") debug_label.pack(anchor='w') debug_box = scrolledtext.ScrolledText(main, height=12, wrap='word', state='disabled') debug_box.pack(fill='both', expand=True, pady=(4,0)) status = ttk.Label(root, textvariable=status_var, relief='sunken', anchor='w') status.pack(side='bottom', fill='x') ui = { 'root': root, 'latest_var': latest_var, 'previous_var': previous_var, 'watched_var': watched_var, 'total_var': total_var, 'status_var': status_var, 'report_box': report_box, 'debug_box': debug_box, } generate_btn.config(command=lambda: generate_report(ui)) copy_btn.config(command=lambda: copy_report(ui)) debug_box.config(state='normal') debug_box.insert(tk.END, f"LATEST_SUNDAY={get_latest_sunday()}\nPREVIOUS_SUNDAY={get_previous_sunday()}\n") debug_box.config(state='disabled') return ui def main(): root = tk.Tk() ui = build_ui(root) root.mainloop() if __name__ == '__main__': try: main() except Exception as e: import traceback, sys, os tb = traceback.format_exc() # Try to show a GUI error dialog if possible try: import tkinter as _tk from tkinter import messagebox as _mb _root = _tk.Tk() _root.withdraw() _mb.showerror("Launch Error", f"Error starting app:\n{e}\n\nA log was written to ~/media_episodes_viewer_error.log") _root.destroy() except Exception: pass # Always write traceback to a known file for debugging try: with open(os.path.expanduser('~/media_episodes_viewer_error.log'), 'w') as fh: fh.write(tb) except Exception: pass # Re-raise so py2app shows the launch error dialog as well raise