221 lines
8.2 KiB
Python
221 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Native GUI using tkinter to generate One Piece progress reports without a web server.
|
|
"""
|
|
|
|
import os
|
|
import threading
|
|
import webbrowser
|
|
from datetime import datetime
|
|
|
|
# Try to import tkinter; if unavailable, fall back to launching the Flask web UI
|
|
try:
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, scrolledtext
|
|
TK_AVAILABLE = True
|
|
except Exception:
|
|
TK_AVAILABLE = False
|
|
|
|
# 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:
|
|
if TK_AVAILABLE:
|
|
main()
|
|
else:
|
|
# Fallback: launch the Flask web UI in a background thread and open the browser
|
|
try:
|
|
from app import app
|
|
except Exception as _e:
|
|
raise RuntimeError(f"Tkinter not available and Flask app failed to import: {_e}")
|
|
|
|
def _run_server():
|
|
port = int(os.environ.get("PORT", 5000))
|
|
app.run(host="127.0.0.1", port=port, debug=False, use_reloader=False)
|
|
|
|
t = threading.Thread(target=_run_server, daemon=True)
|
|
t.start()
|
|
# Give the server a moment to start
|
|
import time
|
|
time.sleep(1.0)
|
|
port = os.environ.get("PORT", "5000")
|
|
try:
|
|
webbrowser.open(f"http://127.0.0.1:{port}/")
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
while t.is_alive():
|
|
t.join(0.5)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
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
|